Errors and limits
Bodies
Section titled “Bodies”Most routes: JSON, one error string, written to be shown to a person.
{ "error": "no such session" }Schema validation failures return the validator’s shape, where message is a
JSON string of the field errors:
{ "success": false, "error": { "name": "ZodError", "message": "[{\"code\":\"too_small\",\"path\":[\"photos\"],\"message\":\"Too small: expected array to have >=1 items\"}]" }}Plain text in two cases: bearer failures (Unauthorized), and the byte routes,
which return a bare 404 Not Found.
Statuses
Section titled “Statuses”| Status | Means |
|---|---|
400 |
Bad body or value, or a malformed Authorization header. Retrying is pointless. |
401 |
Missing or wrong key, or no staff session. |
404 |
No such event, set, session or gallery — or the gallery is not live yet. |
409 |
Session already completed or cancelled, or completed concurrently. Re-read the queue. |
413 |
Body over the contact form’s 16 KB. |
422 |
Photo keys not in R2. missing lists them; nothing was written. |
429 |
Over a rate limit. Wait a minute; there is no Retry-After. |
500 |
Ours. Retry with backoff, then tell us. |
502 / 503 |
Mail provider refused, or unconfigured. |
Presigned PUTs answer from R2, not from this API: S3-shaped XML, and a 403
there is almost always a Content-Type that does not match the signature.
Rate limits
Section titled “Rate limits”| Routes | Limit |
|---|---|
POST /api/auth/sign-in/* |
10/min |
POST /sets/:setId/sessions, POST /track, POST /contact, GET /gallery/:sessionId/zip |
60/min |
Everything else, /booth/* included |
none |
Keyed on CF-Connecting-IP and counted per colo, not globally: a lock on the
door, not flood protection. 60 is loose because a venue shares one NAT IP.
{ "error": "Too many requests. Try again in a minute." }