API & webhooks
Create keys and webhooks in Organization settings. Keys are shown once, can be revoked any time, and are limited to 120 requests per minute.
Authentication
Authorization: Bearer hp_live_xxxxxxxxxxxxxxxx
List booths
Scope experiences:read. Paginate with limit (max 100) and the returned next_cursor.
GET /api/public/v1/experiences?limit=50&cursor=…
{ "data": [{ "id": "…", "title": "Sam & Alex", "occasion": "wedding", "event_id": "…", "created_at": "…" }],
"next_cursor": "2026-09-01T10:00:00Z" }List captures
Scope captures:read. Sealed time-capsule messages and removed photos are never returned.
GET /api/public/v1/captures?experience_id=…&limit=50&cursor=…
Get capture file links
Signed links expire after 10 minutes — fetch fresh ones when you need them.
GET /api/public/v1/captures/{id}/urls
{ "id": "…", "expires_at": "…", "files": [{ "role": "composite", "mime": "image/jpeg", "url": "https://…" }] }Webhooks
Events: capture.created, capture.approved, experience.live, guestbook.entry (message content is not included). We POST JSON and retry failures with backoff for up to 7 attempts. Every attempt is shown in the delivery log, where you can resend.
POST https://your-server/webhook
X-Webhook-Event: capture.created
X-Webhook-Id: <delivery id>
X-Webhook-Signature: t=1727000000,v1=<hex HMAC-SHA256>
{ "id": "…", "type": "capture.created", "created_at": "…",
"data": { "capture_id": "…", "experience_id": "…", "event_id": "…", "booth_id": "…", "kind": "strip" } }Verify: compute HMAC-SHA256 with your signing secret over `${t}.${rawBody}`, compare to v1 in constant time, and reject timestamps older than 5 minutes.
const [t, v1] = sig.split(",").map((p) => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));