Errors & rate limits
{ "success": false, "error": { "code": "SLOT_TAKEN", "message": "That slot was just taken. Pick another time." } }Branch on error.code — it is stable. error.message is English and safe to display, but your own localised copy will read better.
Not found / not available
Section titled “Not found / not available”| Status | Code | Cause | What to do |
|---|---|---|---|
| 404 | RESOURCE_NOT_FOUND | Unknown slug, room, hold or token | Check the identifier |
| 404 | SITE_NOT_LIVE | The studio unpublished its site | Hide booking; show phone / WhatsApp |
| 403 | STUDIO_NOT_VERIFIED | Ownership not confirmed yet | Studio-side; nothing a guest can do |
| 409 | ROOM_NOT_BOOKABLE | Online booking off, or room unpriced/inactive | Remove it from your room list |
Validation
Section titled “Validation”| Status | Code | Cause | What to do |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Malformed field, bad duration, idempotency key reused with a different body | Fix the payload; re-derive durations from the room |
| 400 | CAPTCHA_FAILED | Captcha token missing, expired or already used | Reset the widget, get a fresh token |
| 400 | PAYMENT_MODE_DENIED | Mode not in room.paymentModes | Offer only the modes the room allows |
Timing and conflicts
Section titled “Timing and conflicts”| Status | Code | Cause | What to do |
|---|---|---|---|
| 409 | OUTSIDE_HOURS | The room is closed then | Refetch availability — your span maths drifted |
| 409 | OUTSIDE_BOOKING_WINDOW | Breaks minNoticeMin or maxAdvanceDays | Clamp the calendar to the window |
| 409 | SLOT_TAKEN | Someone booked it first | Refetch, keep the guest’s details, offer another time |
| 410 | HOLD_EXPIRED | 12 minutes passed | Restart from the hold with a new idempotency key |
| 409 | HOLD_NOT_VERIFIED | Confirming before verifying | Send the guest back to the code step |
| 409 | HOLD_ALREADY_CONFIRMED | Double submit | Show the existing booking |
| 409 | CANCEL_TOO_LATE | Inside the cancellation window | Show the studio’s phone |
| 409 | INVALID_STATE | The action makes no sense now | Re-read the booking |
Codes and limits
Section titled “Codes and limits”| Status | Code | Cause | What to do |
|---|---|---|---|
| 400 | CODE_INVALID | Wrong or expired code | Let them retype; show attempts left |
| 429 | TOO_MANY_ATTEMPTS | 5 wrong codes | The hold is burnt — restart |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests | Back off, see below |
Infrastructure
Section titled “Infrastructure”| Status | Code | Cause | What to do |
|---|---|---|---|
| 409 | PAYMENTS_UNAVAILABLE | Stripe not configured | Offer on_site |
| 503 | SMS_UNAVAILABLE | SMS provider down | Offer channel: "email" |
| 500 | INTERNAL_ERROR | Our fault | Retry once; include X-Request-ID if you report it |
Rate limits
Section titled “Rate limits”| Scope | Limit |
|---|---|
Reads (GET /public/*) | 120 req/min per IP |
| Writes (holds, verify, resend, confirm, cancel) | 15 req/min per IP |
| Holds per contact | 5 per hour |
| Holds per IP | 20 per hour |
| Code resends | 3 per hold, ≥ 60 s apart |
| Verification attempts | 5 per hold |
Exceeding any of them returns 429 RATE_LIMIT_EXCEEDED.
Retrying safely
Section titled “Retrying safely”GETs are safe to retry.POST /holdsis safe to retry with the sameidempotencyKey— you get the same hold back, no second SMS.verify,confirmandcancelare not idempotent. On a network timeout, re-read state (GET /holds/{id}is not public, so use the manage endpoint once a booking exists) before firing again.- Back off exponentially on
429and5xx. Never retry a4xxother than429without changing the request.