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, already used, solved for another action or on a host that is not the studio’s | Reset the widget, get a fresh token on the studio’s host with action hold |
| 400 | PAYMENT_MODE_DENIED | Mode not in room.paymentModes | Offer only the modes the room allows |
| 400 | GROUP_SIZE_INVALID | groupSizeId not offered by the room, or partySize outside 1…capacity | Offer only the room’s group sizes; clamp the people counter to capacity |
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 | expiresAt passed (5 minutes unverified, 12 minutes after creation once verified) | 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 |
API keys (studio endpoints)
Section titled “API keys (studio endpoints)”| Status | Code | Cause | What to do |
|---|---|---|---|
| 401 | KEY_INVALID | Unknown or malformed key | Check the key you stored |
| 401 | KEY_REVOKED | The studio revoked the key | Ask the studio for a new key |
| 401 | KEY_EXPIRED | Rotated more than 7 days ago | Switch to the replacement key |
| 403 | KEY_ENV_MISMATCH | Test key on production, or live key elsewhere | Use the key for this environment |
| 403 | SCOPE_MISSING | The key lacks details.scope | Ask for a key with that scope |
| 403 | KEY_ROUTE_NOT_ALLOWED | Keys cannot call this endpoint | Do it in the dashboard |
Infrastructure
Section titled “Infrastructure”| Status | Code | Cause | What to do |
|---|---|---|---|
| 409 | PAYMENTS_UNAVAILABLE | Stripe not configured | Offer on_site |
| 422 | PHONE_NOT_SUPPORTED | Not a mobile number in a country we text (premium-rate, shared-cost, landline, VoIP and foreign numbers are refused) | Offer channel: "email" |
| 503 | SMS_UNAVAILABLE | Texts are paused: provider down, the daily SMS budget is used up, or SMS is switched off for this studio | Offer channel: "email" (it keeps working) |
| 503 | HOLDS_UNAVAILABLE | Online booking is paused for this studio | Show the studio’s contact details |
| 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 code (a 6th wrong code burns the hold), counted atomically |
| Unverified holds per studio | 3 at a time (sandbox sites excepted); 429 RATE_LIMIT_EXCEEDED beyond |
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.