Moments are user-generated, event-captured highlights — short video and photo records with engagement metadata. Fetch them programmatically for display on external surfaces. Every read scopes to your creator account: foreign, unpublished, or deleted moments are indistinguishable 404s, never partial data.
moments.read scope (granted via the moments product as moments.*). Secret keys travel in P-Sec-Key; publishable keys in P-Pub-Key or ?pPubKey=, subject to origin allowlisting on browser calls. Key model in Authentication.event_id you own (and optionally division and highlight ids) to read from.GET /v1/events/{eventId}/highlights returns every published highlight for one of your events, newest first (created_at descending, id descending).
GET /v1/events/{eventId}/highlights
eventIdstringPathRequired—Your event id. Another creator’s id fails the whole request with 404 Event not found — ownership is checked before any data loads.
limitinteger 1–100QueryOptional25Items per page. Out-of-range values fail validation with 400 — never clamped. The effective value echoes back in pagination.limit.
cursorstringQueryOptionalfirst pageBase64url page cursor from the previous response’s next_cursor. Omit for the first page. Empty, invalid, or negative cursors restart at the first page with no error. Never construct cursors — the encoding is internal and may change.
——BodyOptional—None. Reads carry no body.
curl "https://api.pulluptoplay.com/v1/events/0hspDRQ-W3Bws3e3/highlights?limit=10&pPubKey=$PULLUP_PUBLIC_KEY"const params = new URLSearchParams({ pPubKey: process.env.
import os
import requests
def read_event_highlights(event_id: str, limit: int = 10) -> dict:
response = requests.get(
f"https://api.pulluptoplay.com/v1/events/{event_id}/highlights",
params={"pPubKey": os.environ["PULLUP_PUBLIC_KEY"], "limit": limit},
timeout=30,
)
if not response.ok:
{
"status": "success",
"code": 200,
"message": "Moments retrieved",
"data": {
"moments": [
{
"id": "EQvwGMnOXd3oevo-3BzyTd",
"event_id": "0hspDRQ-W3Bws3e3",
"event_division_id": null,
"description": "Final check",
"location": { "place": "Lagos" },
"mentions": [],
Every nullable above arrives explicitly as null — absent asset dimensions, locations, and creator avatars are never omitted. Asset metrics count engagement; metadata.creator identifies the posting account. Parse assets defensively: entries may carry kind values beyond VIDEO as capture modes evolve.
GET /v1/events/{eventId}/divisions/{eventDivisionId}/highlights narrows the same read to one division. The division must belong to the event and the event must be yours — anything else is 404.
GET /v1/events/{eventId}/divisions/{eventDivisionId}/highlights
eventIdstringPathRequired—Your event id. Foreign ids fail with 404 Event not found.
eventDivisionIdstringPathRequired—A division of that event. A division outside your events fails the whole request with 404 Event division not found.
limitinteger 1–100QueryOptional25Items per page. Out-of-range values fail validation with 400 — never clamped.
cursorstringQueryOptionalfirst pageBase64url page cursor. Empty, invalid, or negative values restart at the first page with no error.
——BodyOptional—None. Reads carry no body.
curl "https://api.pulluptoplay.com/v1/events/0hspDRQ-W3Bws3e3/divisions/vip-gate/highlights?pPubKey=$PULLUP_PUBLIC_KEY"const params = new URLSearchParams({ pPubKey: process.env.PULLUP_PUBLIC_KEY! });
const response = await fetch(
`https://api.pulluptoplay.com/v1/events/0hspDRQ-W3Bws3e3/divisions/vip-gate/highlights?${params}`
import os
import requests
def read_division_highlights(event_id: str, division_id: str) -> dict:
response = requests.get(
f"https://api.pulluptoplay.com/v1/events/{event_id}/divisions/{division_id}/highlights",
params={"pPubKey": os.environ["PULLUP_PUBLIC_KEY"]},
timeout=30,
)
if not response.ok:
{
"status": "success",
"code": 200,
"message": "Moments retrieved",
"data": {
"moments": [
{
"id": "EQvwGMnOXd3oevo-3BzyTd",
"event_id": "0hspDRQ-W3Bws3e3",
"event_division_id": "vip-gate",
"description": "Final check",
"location": { "place": "Lagos" },
"mentions": [],
Full parameter contracts for every read on this page also live in Highlights API.
GET /v1/highlights/{highlightId} returns one published highlight you own — or 404 when it is missing, unpublished, deleted, or someone else’s. The three cases are deliberately indistinguishable: never branch access decisions on response differences that do not exist.
GET /v1/highlights/{highlightId}
highlightIdstringPathRequired—The highlight id. Anything unresolvable returns 404 Moment not found.
——Query / BodyOptional—None. Authentication travels in P-Sec-Key, P-Pub-Key, or ?pPubKey= — the examples below use ?pPubKey= so no custom headers are needed.
curl "https://api.pulluptoplay.com/v1/highlights/EQvwGMnOXd3oevo-3BzyTd?pPubKey=$PULLUP_PUBLIC_KEY"const params = new URLSearchParams({ pPubKey: process.env.PULLUP_PUBLIC_KEY! });
const response = await fetch(
`https://api.pulluptoplay.com/v1/highlights/EQvwGMnOXd3oevo-3BzyTd?${params}`
);
if (!response.ok) {
import os
import requests
def read_highlight(highlight_id: str) -> dict:
response = requests.get(
f"https://api.pulluptoplay.com/v1/highlights/{highlight_id}",
params={"pPubKey": os.environ["PULLUP_PUBLIC_KEY"]},
timeout=30,
)
if not response.ok:
failure = response.json()
raise RuntimeError(
{
"status": "success",
"code": 200,
"message": "Moment retrieved",
"data": {
"moment": {
"id": "EQvwGMnOXd3oevo-3BzyTd",
"event_id": "0hspDRQ-W3Bws3e3",
"event_division_id": null,
"description": "Final check",
"location": null,
"mentions": [],
"assets": [],
"metrics"
Note the wrapper: single reads return { moment }, lists return { moments, pagination }. Code that handles both shapes must branch on the key — a list parser fed a single read silently yields nothing.
Standard reads cover one event or division at a time. GET /v1/events/highlights spans every event you own with no id required — ownership derives from the key. POST /v1/events/highlights/query filters explicit id lists; POST exists because the filter body is too expressive for a query string, and nothing is created, updated, or deleted.
GET /v1/events/highlights
limitinteger 1–100QueryOptional25Items per page. Out-of-range values fail validation with 400 — never clamped.
cursorstringQueryOptionalfirst pageBase64url page cursor. Empty, invalid, or negative values restart at the first page with no error.
——Path / BodyOptional—None. No event id is needed — the key selects the scope.
curl "https://api.pulluptoplay.com/v1/events/highlights?limit=20&pPubKey=$PULLUP_PUBLIC_KEY"const params = new URLSearchParams({ pPubKey: process.env.PULLUP_PUBLIC_KEY!, limit: "20" });
const response = await fetch(`https://api.pulluptoplay.com/v1/events/highlights?${params}`);
if
import os
import requests
def read_global_feed(limit: int = 20) -> dict:
response = requests.get(
"https://api.pulluptoplay.com/v1/events/highlights",
params={"pPubKey": os.environ["PULLUP_PUBLIC_KEY"], "limit": limit},
timeout=30,
)
if not response.ok:
failure = response.json()
raise RuntimeError(
{
"status": "success",
"code": 200,
"message": "Moments retrieved",
"data": {
"moments": [
{
"id": "EQvwGMnOXd3oevo-3BzyTd",
"event_id": "0hspDRQ-W3Bws3e3",
"event_division_id": null,
"description": "Final check",
"location": { "place": "Lagos" },
"mentions": [],
An account with no owned events returns "moments": [], never an error.
POST /v1/events/highlights/query
event_idsstring[]BodyOptional[]Up to 50 event ids. Empty scopes to every event you own. Entries trim; blank entries and lists over 50 fail validation with 400.
event_division_idsstring[]BodyOptional[]Up to 50 division ids with identical trimming and length rules. Each must belong to an event you own.
limitinteger 1–100BodyOptional25Items per page. Out-of-range values fail validation with 400 — never clamped.
cursorstringBodyOptionalfirst pageBase64url page cursor. Empty, invalid, or negative values restart at the first page with no error.
——Path / QueryOptional—None. Filters travel in the JSON body.
Ownership is enforced per id: any event you do not own — or any division outside your events — fails the whole request with 404. Validate id lists before sending, or catch the 404 and drop the offending id.
curl -X POST https://api.pulluptoplay.com/v1/events/highlights/query \
-H "Content-Type: application/json" \
-H "P-Sec-Key: $PULLUP_SECRET_KEY" \
-d '{
"event_ids": ["0hspDRQ-W3Bws3e3", "hw4EzFN5oCZQLDYJ"],
"event_division_ids": ["7f1c9a2e-4b6d-4e8f-9a0b-1c2d3e4f5a6b"],
"limit": 20
}'const response = await fetch("https://api.pulluptoplay.com/v1/events/highlights/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
"P-Sec-Key": process.env.
import os
import requests
def query_highlights(event_ids: list[str], division_ids: list[str], limit: int = 20) -> dict:
response = requests.post(
"https://api.pulluptoplay.com/v1/events/highlights/query",
headers={"P-Sec-Key": os.environ["PULLUP_SECRET_KEY"]},
json={
"event_ids": event_ids,
"event_division_ids": division_ids,
"limit": limit,
},
timeout
{
"status": "success",
"code": 200,
"message": "Moments retrieved",
"data": {
"moments": [
{
"id": "EQvwGMnOXd3oevo-3BzyTd",
"event_id": "hw4EzFN5oCZQLDYJ",
"event_division_id": "7f1c9a2e-4b6d-4e8f-9a0b-1c2d3e4f5a6b",
"description": "Final check",
"location": null,
"mentions": [],
"assets"
Every list endpoint paginates identically — event reads, division reads, the global feed, and bulk queries. Page numbers would break under live data (publishes between page 1 and page 2 shift every subsequent page, duplicating and skipping items), so cursors pin your position in an ordered stream instead. New publishes append at the head; your walk through older items stays stable regardless of mid-read arrivals.
Cursors are base64url-encoded row offsets. Never construct, parse, or store them beyond passing the next one back — the encoding is internal and may change.
limitinteger 1–100Query (GET) / Body (bulk POST)Optional25Items requested per page. Out-of-range values fail validation with 400 — rejected, never clamped. Echoed back in pagination.limit.
cursorstringQuery (GET) / Body (bulk POST)Optionalfirst pageOpaque cursor from the previous page. Omit for the first page. Empty, unparseable, or negative values restart at offset zero with no error.
Every page returns the same envelope. has_more decides continuation; next_cursor is null on the final page:
{
"status": "success",
"code": 200,
"message": "Moments retrieved",
"data": {
"moments": [],
"pagination": {
"limit": 25,
"has_more"
const all: unknown[] = [];
let cursor: string | undefined;
do {
const params = new URLSearchParams({
pPubKey: process.env.PULLUP_PUBLIC_KEY!,
limit: "100",
...(cursor ? { cursor } : {}),
});
const response = await fetch(
`https://api.pulluptoplay.com/v1/events/highlights?${
import os
import requests
def drain_global_feed(limit: int = 100) -> list:
all_moments = []
cursor = None
while True:
params = {"pPubKey": os.environ["PULLUP_PUBLIC_KEY"], "limit": limit}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://api.pulluptoplay.com/v1/events/highlights"
CURSOR=""
while :; do
BODY=$(curl -s "https://api.pulluptoplay.com/v1/events/highlights?limit=100&cursor=${CURSOR}&pPubKey=$PULLUP_PUBLIC_KEY")
echo "$BODY" | python3 -c "import json,sys; [print(m['id']) for m in json.load(sys.stdin)['data']['moments']]"
MORE=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['pagination']['has_more'])")
[ "$MORE" =
moments.read scope and key transport.