Skip to content

Kiosk, gallery and tracking

No credential, rate limited instead.

What a kiosk loads from the two slugs in its URL.

200 { set: { id, slug, name }, event: { id, slug, name } } 404 { "error": "no such set" }

Creates the active session the booth picks up.

{
"guests": [
{ "name": "Alex Reyes", "email": "alex@example.com" },
{ "name": "Sam", "phone": "0412 345 678" }
]
}

1–12 guests. One name each, stored as first word and rest. At least one of email or phone, since that is how the gallery link reaches them; phones are normalised to E.164, defaulting to a US country code. A guest who has been to an event before links to the same customer on email, then phone, so a placeholder address merges two people.

201 { id, label, status: "active" }. The gallery is GALLERY_URL/<id> once live. 400 { "error": "This doesn't look like a phone number: 04123" } or { "error": "Sam needs an email or a phone number" } — both written to show the guest as they are. 404 { "error": "no such set" }

A gallery is a completed session with photos. Until then these 404.

The event page: its sets, each with its latest 12 live sessions, newest first. No paging.

{
"id": "7Kq2mZ0aBx91Tc",
"slug": "city-launch",
"name": "City Launch",
"startAt": 1758499200000,
"sets": [
{
"id": "Rd8vN3pLw0Ymqe",
"slug": "set-1",
"name": "Set 1",
"sessions": [
{
"id": "Mv6xC1kRp8Wdzn",
"label": "Alex and Sam",
"createdAt": 1758501330000,
"photos": [
{
"id": "Yn2fB6jWc4Qaol",
"url": "https://cdn.theloungebooth.com/sessions/Mv6xC1kRp8Wdzn/Zp3nT8wQ1eLk52.jpg",
"width": 2048,
"height": 2560
}
]
}
]
}
]
}

404 { "error": "no such event" }, also for a private event. Private hides the index, not the session links.

200 { id, label, event: { id, slug, name, startAt }, photos: [{ id, url, width, height }] } in gallery order. 404 { "error": "no such gallery" } while active, cancelled, or without photos.

Photo urls are on cdn.theloungebooth.com, the bucket’s own domain, cached at the edge. Rendering one is not counted as a download.

GET /gallery/:sessionId/photos/:photoId/download

Section titled “GET /gallery/:sessionId/photos/:photoId/download”

200 the bytes, Content-Disposition: attachment, filename from the label (Alex and Sam 1.jpg). Counted as a download. 404 plain text.

200 every photo as one stored zip, streamed as it is built. Counted as a zip. 404 plain text. 429 over 60/min.

Views and share clicks. Downloads and zips count themselves in the routes above.

text/plain with a JSON body, so sendBeacon posts without a preflight and survives the page closing.

navigator.sendBeacon(
"https://api.theloungebooth.com/track",
JSON.stringify({ kind: "view", sessionId: "Mv6xC1kRp8Wdzn" }),
)
  • { "kind": "view", "sessionId": "…" } or { "kind": "view", "eventId": "…" }
  • { "kind": "share", "sessionId": "…", "channel": "facebook" }facebook, instagram, tiktok, copy or native

204 counted, no body. 400 { "error": "bad body" } or { "error": "no such session or event" }.

Rows carry kind, channel, event, session and a timestamp. Nothing about the visitor: no IP, no user agent, no cookie.

Labels are checked against the published Sanity contact page, so this is not a general mailer.

{ "fields": { "Name": "Alex Reyes", "Email": "alex@example.com", "Message": "" } }

24 fields, 2,000 characters each, 16 KB total.

200 { "ok": true } 400 { "error": "Unknown fields: Company" } or { "error": "Invalid submission" } 413 { "error": "Message too long" } 502 / 503 Resend refused, or the keys are unset.

GET /photos/* reads from the simulated bucket and PUT /photos/* is what a presigned URL points at with no R2 credentials to sign against. A deployed Worker 404s the PUT. In production, photo URLs come back on the gallery responses, on cdn.theloungebooth.com.