Booth API
Everything the booth does goes through the routes on this page, each called with your API key. The photos themselves never pass through the API: you upload them straight to storage on signed URLs it gives you.
A session reaches the booth one of two ways. Usually guests register at our kiosk and the session waits in the set’s queue for you to pick up. At an event without a kiosk, the booth takes the guests’ details itself and creates the session. From there the flow is the same: upload, then complete.
Walkthrough
Section titled “Walkthrough”-
Find your set. List the events running now and pick the set (station) this booth is working.
Terminal window curl -sS https://api.theloungebooth.com/events \-H "Authorization: Bearer $TLB_API_KEY"{"events": [{"id": "7Kq2mZ0aBx91Tc","slug": "city-launch","name": "City Launch","startAt": 1758499200000,"endAt": null,"venueMode": false,"sets": [{ "id": "Rd8vN3pLw0Ymqe", "slug": "set-1", "name": "Set 1" },{ "id": "Jf4bQ7tHs2Xzvi", "slug": "set-2", "name": "Set 2" }]}]}Keep the set’s
id. Every later call starts from it. -
Get a session. Either take the oldest group waiting in the queue:
Terminal window curl -sS https://api.theloungebooth.com/sets/Rd8vN3pLw0Ymqe/sessions \-H "Authorization: Bearer $TLB_API_KEY"{"sessions": [{"id": "Mv6xC1kRp8Wdzn","label": "Alex and Sam","createdAt": 1758501330000,"guests": [{ "firstName": "Alex", "lastName": "Reyes" },{ "firstName": "Sam", "lastName": "Okafor" }]}]}Or, with no kiosk, create one from the details the booth collected:
Terminal window curl -sS -X POST \https://api.theloungebooth.com/sets/Rd8vN3pLw0Ymqe/sessions \-H "Authorization: Bearer $TLB_API_KEY" \-H "Content-Type: application/json" \-d '{ "guests": [{ "firstName": "Alex", "lastName": "Reyes", "email": "alex@example.com" }] }'{ "id": "Mv6xC1kRp8Wdzn", "label": "Alex", "status": "active" }Either way, the session’s
idis what the next three steps take. -
Get upload URLs. One per photo. Ask for the whole session at once with
count:Terminal window curl -sS -X POST \"https://api.theloungebooth.com/sessions/Mv6xC1kRp8Wdzn/uploads?count=2" \-H "Authorization: Bearer $TLB_API_KEY"{"uploads": [{"key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Zp3nT8wQ1eLk52.jpg","url": "https://<account>.r2.cloudflarestorage.com/tlb-photos/7Kq2mZ0aBx91Tc/…&X-Amz-Expires=600"},{"key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Hb9dK4sVu7Rm03.jpg","url": "https://<account>.r2.cloudflarestorage.com/tlb-photos/7Kq2mZ0aBx91Tc/…&X-Amz-Expires=600"}],"contentType": "image/jpeg","expiresIn": 600} -
Upload each photo to its
url, with thecontentTypeyou were given and no API key (this goes to storage, not to us):Terminal window curl -sS -X PUT "$url" \-H "Content-Type: image/jpeg" \--data-binary @frame-01.jpg -
Complete the session with the keys you uploaded, in the order the gallery should show them. This publishes the gallery and sends each guest their link.
Terminal window curl -sS -X POST \https://api.theloungebooth.com/sessions/Mv6xC1kRp8Wdzn/complete \-H "Authorization: Bearer $TLB_API_KEY" \-H "Content-Type: application/json" \-d '{"photos": [{ "key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Zp3nT8wQ1eLk52.jpg", "width": 2048, "height": 2560 },{ "key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Hb9dK4sVu7Rm03.jpg", "width": 2048, "height": 2560 }]}'{ "id": "Mv6xC1kRp8Wdzn", "status": "completed", "completedAt": 1758501902000, "photos": 2 }
If the group leaves without photos, cancel the session instead of completing it.
Reference
Section titled “Reference”Send Authorization: Bearer $TLB_API_KEY on every route. A wrong key, or one
since replaced or revoked, gets 401. Request and response bodies are JSON. Ids are 14-character strings
such as Mv6xC1kRp8Wdzn, and case matters. Times are whole milliseconds since
January 1, 1970 UTC, the same number JavaScript’s Date.now() gives. Error
bodies and the validation shape are on Errors.
GET /events
Section titled “GET /events”The events a booth can work now, each with its sets. No parameters.
An event is listed from a day before it starts until a day after it ends, so you can set up early and upload late. An event with no end stays listed. Events come oldest start first; sets within an event by name. An event with no sets is left out.
| Field | Type | What it is |
|---|---|---|
events |
array | The events, possibly empty. |
events[].id |
id | The event. |
events[].slug |
string | The event’s short name in our links, such as city-launch in kiosk.theloungebooth.com/city-launch/set-1. Also accepted by GET /events/:eventSlug/sets/:setSlug. |
events[].name |
string | The event’s name, for showing to your operator. |
events[].startAt |
time | When the event starts. |
events[].endAt |
time or null |
When it ends. null means we set no end. |
events[].venueMode |
boolean | Whether the event runs on the venue server rather than in the cloud. The routes are the same either way; show it so your operator knows which they’re on. |
events[].sets |
array, at least 1 | The event’s sets. A set is one booth station; most events have one. |
events[].sets[].id |
id | The set. Every other route starts from it. |
events[].sets[].slug |
string | The set’s short name in the kiosk link, such as set-1. Also accepted by GET /events/:eventSlug/sets/:setSlug. |
events[].sets[].name |
string | The set’s name, such as Set 1 or Patio. |
GET /events/:eventSlug/sets/:setSlug
Section titled “GET /events/:eventSlug/sets/:setSlug”Finds one set from the two short names in its kiosk link, such as
kiosk.theloungebooth.com/city-launch/set-1. Use it if your booth is set up
from that link rather than from a list.
| Path | What it is |
|---|---|
eventSlug |
The event’s short name, such as city-launch. |
setSlug |
The set’s short name, such as set-1. |
{ "set": { "id": "Rd8vN3pLw0Ymqe", "slug": "set-1", "name": "Set 1" }, "event": { "id": "7Kq2mZ0aBx91Tc", "slug": "city-launch", "name": "City Launch", "venueMode": false }}| Field | Type | What it is |
|---|---|---|
set.id |
id | The set. The queue and create routes take it. |
set.slug, set.name |
string | As in GET /events. |
event.id, event.slug, event.name, event.venueMode |
id, string, string, boolean | The set’s event, as in GET /events. |
Unlike GET /events, this finds the set whether or not its event is running.
| Status | Body | When |
|---|---|---|
404 |
{ "error": "no such set" } |
No event has that short name, or it has no set by that name. |
GET /sets/:setId/sessions
Section titled “GET /sets/:setId/sessions”The set’s queue: sessions waiting for photos, oldest first. Take the first one unless your operator picks another.
| Path | What it is |
|---|---|
setId |
events[].sets[].id from GET /events. An unknown id gives an empty queue, not an error. |
| Field | Type | What it is |
|---|---|---|
sessions |
array | The waiting sessions, possibly empty. Completed and canceled sessions never appear. |
sessions[].id |
id | The session. The upload, complete and cancel routes take it. |
sessions[].label |
string | The group’s first names as one line, such as Alex and Sam or Priya, Marcus, and Elena. It is the gallery’s heading; show it to your operator so they call the right group. |
sessions[].createdAt |
time | When the group registered. |
sessions[].guests |
array, 1–12 | Who is in the group, in the order they registered. |
sessions[].guests[].firstName |
string | As the guest typed it. Never empty. |
sessions[].guests[].lastName |
string | As the guest typed it. "" when they left it out. |
Guests’ emails and phone numbers are never returned.
POST /sets/:setId/sessions
Section titled “POST /sets/:setId/sessions”Registers a group on the set and puts it in the queue. Call it at events
without our kiosk, with the details your booth collected. It only works while
the event is running, the same window as GET /events: from a
day before it starts until a day after it ends.
| Path | What it is |
|---|---|
setId |
events[].sets[].id from GET /events. |
{ "guests": [ { "firstName": "Alex", "lastName": "Reyes", "email": "alex@example.com" }, { "firstName": "Sam", "phone": "(415) 555-0100" }, { "firstName": "Jo", "email": "jo@example.com", "phone": "212-555-0199" } ]}| Field | Type | What it is |
|---|---|---|
guests |
array, 1–12, required | Everyone in the group, one entry each. |
guests[].firstName |
string, up to 80, required | Shown in the gallery heading. Spaces at either end are trimmed; blank is refused. |
guests[].lastName |
string, up to 80, optional | Kept for our records. Leave it out or send "" if you don’t ask for it. |
guests[].email |
string, up to 254, this or phone |
Where we email the gallery link. Stored in lowercase. |
guests[].phone |
string, up to 32, this or email |
Where we text the gallery link. Spaces, dashes and brackets are ignored. Without a leading + it must be a real 10-digit US or Canadian number; any other country needs + and its country code. |
Give each guest an email, a phone number, or both. With both, the link goes both ways. A guest who has been to one of our events before is matched to their earlier visits by email, then by phone.
Response, 201 Created:
{ "id": "Mv6xC1kRp8Wdzn", "label": "Alex, Sam, and Jo", "status": "active" }| Field | Type | What it is |
|---|---|---|
id |
id | The new session. Take it straight to uploads. Its gallery will be gallery.theloungebooth.com/<id>. |
label |
string | The gallery heading, built from the first names. |
status |
"active" |
Waiting for photos. It is also in the set’s queue now. |
Nobody is sent anything yet. Each guest gets the link when you complete the session.
| Status | Body | When |
|---|---|---|
400 |
{ "error": "Every guest needs a first name." } |
A guest’s firstName is missing or blank. |
400 |
{ "error": "Sam needs an email or a phone number" } |
A guest gave neither. |
400 |
{ "error": "This doesn’t look like a phone number: 555-01" } |
A phone number we can’t read. |
400 |
{ "error": "Add at least one guest." } |
An empty guests. |
400 |
validation error | More than 12 guests, a malformed email, or a value over its length. |
404 |
{ "error": "no such set" } |
No set has that setId. |
409 |
{ "error": "This event isn’t taking registrations right now." } |
The event starts more than a day from now, or ended more than a day ago. |
409 |
{ "error": "This event registers at the venue." } |
The event runs in venue mode and you reached the cloud. Point the kiosk or booth at the venue server. |
429 |
{ "error": "Too many requests. Try again in a minute." } |
More than 60 creates in a minute from your IP address. |
The guest errors are written for the guest to read, so you can show them as they are.
POST /sessions/:id/uploads
Section titled “POST /sessions/:id/uploads”Signed upload URLs for a session’s photos, one per photo. No request body.
| Path | What it is |
|---|---|
id |
The session, from the queue or from creating it. |
| Query | Type | What it is |
|---|---|---|
count |
whole number, 1–200, optional | How many URLs to get. Leave it out for one, in the single shape below. |
type |
jpeg or png, optional |
The format of every photo in this call. Default jpeg. A session with both makes one call per type. |
With count, the response lists the uploads:
{ "uploads": [ { "key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Zp3nT8wQ1eLk52.jpg", "url": "https://<account>.r2.cloudflarestorage.com/tlb-photos/7Kq2mZ0aBx91Tc/…" } ], "contentType": "image/jpeg", "expiresIn": 600}Without it, one upload, not in a list:
{ "key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Zp3nT8wQ1eLk52.jpg", "url": "https://<account>.r2.cloudflarestorage.com/tlb-photos/7Kq2mZ0aBx91Tc/…", "contentType": "image/jpeg", "expiresIn": 600}| Field | Type | What it is |
|---|---|---|
key |
string | Where this photo will live: <event id>/<session id>/<photo id>.jpg (or .png). Keep it; complete takes it. Use it as given rather than building your own. |
url |
string | Where to upload the photo, with an HTTP PUT of its raw bytes. No API key: the signature in the URL is the permission. |
contentType |
"image/jpeg" or "image/png" |
The exact Content-Type header to send with every PUT from this call. It is part of the signature, so anything else, even image/jpeg; charset=utf-8, gets 403 from storage. |
expiresIn |
seconds, always 600 |
How long each url works. Start each upload within ten minutes. For a big session on a slow line, ask in batches as you go. |
Every call gives new keys. A failed upload can be retried on the same url
until it expires; after that, ask for a new one.
| Status | Body | When |
|---|---|---|
400 |
validation error | count isn’t a whole number from 1 to 200, or type isn’t jpeg or png. |
404 |
{ "error": "no such session" } |
No session has that id. |
409 |
{ "error": "session is completed" } or "session is cancelled" |
The session no longer takes photos. |
POST /sessions/:id/complete
Section titled “POST /sessions/:id/complete”Records the uploaded photos, publishes the gallery and sends each guest their link. Call it once per session, after every upload has finished.
| Path | What it is |
|---|---|
id |
The session. |
{ "photos": [ { "key": "7Kq2mZ0aBx91Tc/Mv6xC1kRp8Wdzn/Zp3nT8wQ1eLk52.jpg", "width": 2048, "height": 2560 } ]}| Field | Type | What it is |
|---|---|---|
photos |
array, 1–200, required | The photos in gallery order. |
photos[].key |
string, required | A key from uploads for this session whose upload succeeded. |
photos[].width |
whole number, above 0, required | The photo’s width in pixels. The gallery lays out its grid from width and height before the image loads. |
photos[].height |
whole number, above 0, required | The photo’s height in pixels. |
We check that every key belongs to this session and is really in storage before saving anything. Photos staff already added in admin stay first; yours follow.
Response, 200 OK:
{ "id": "Mv6xC1kRp8Wdzn", "status": "completed", "completedAt": 1758501902000, "photos": 1 }| Field | Type | What it is |
|---|---|---|
id |
id | The session. |
status |
"completed" |
The gallery is live at gallery.theloungebooth.com/<id>. |
completedAt |
time | When it went live. |
photos |
whole number | How many photos this call added. |
Links go out by email, text or both, whichever each guest gave. Guests already sent their link are not sent it again.
| Status | Body | When |
|---|---|---|
400 |
validation error | No body, an empty photos, the same key twice, or a width or height that isn’t a whole number above 0. |
404 |
{ "error": "no such session" } |
No session has that id. |
409 |
{ "error": "session is completed" } or "session is cancelled" |
Already completed or canceled. |
409 |
{ "error": "session completed concurrently" } |
Two completes for the same session crossed; the other one won. |
422 |
{ "error": "objects not found", "missing": ["7Kq2…/Mv6x…/Zp3n….jpg"] } |
A key was never uploaded, its URL expired first, or it belongs to another session. missing lists them. Nothing was saved: upload those again and call again with the full list. |
POST /sessions/:id/cancel
Section titled “POST /sessions/:id/cancel”Takes a session out of the queue without photos, for a group that left. No request body. It can’t be undone, and only a waiting session can be canceled.
| Path | What it is |
|---|---|
id |
The session. |
Response, 200 OK:
{ "id": "Mv6xC1kRp8Wdzn", "status": "cancelled" }| Field | Type | What it is |
|---|---|---|
id |
id | The session. |
status |
"cancelled" |
Out of the queue for good. The group registers again for a new session. |
| Status | Body | When |
|---|---|---|
404 |
{ "error": "no active session with that id" } |
No such session, or it is already completed or canceled. |
Retries
Section titled “Retries”Only this page’s create route is rate limited: 60 a minute from one IP address,
answered with 429. Nothing is idempotent: two creates make two sessions, two uploads calls give two sets of
keys, and a second complete gets 409. After a timeout, read the queue before
creating again.