Guides
Errors and limits
Every failure has the same shape and a stable code. Branch on the code, never on the prose.
{
"error": {
"code": "forbidden",
"message": "This key does not carry the \"customers:read\" scope. Its scopes are: catalog:read, availability:read."
}
}Codes are a closed set and will not change meaning; messages are free to improve, so an integration that matches on wording will break and one that matches on error.code will not. A invalid_request also carries details naming the field at fault, and a refused write carries the exact reason — see below.
Codes
| Status | Code | What it means | What to do |
|---|---|---|---|
| 401 | unauthorized | No key, or one that is unknown, revoked or expired. | Do not retry with the same key. Check it, or mint a new one. |
| 403 | forbidden | The key is valid but does not carry the scope this route needs. | Retrying will never help. The owner must issue a key with that scope. |
| 403 | plan_required | The business’s plan does not include API access, or its booking allowance is used up. | The owner needs to upgrade. Surface this rather than retrying. |
| 404 | not_found | No such record on this account — on a read, or as an id in a write. | Also what you get for a record belonging to another business — the API will not confirm it exists. force never changes it. |
| 405 | method_not_allowed | A real endpoint, called with a method it does not take. | The Allow header names the methods it does take. |
| 409 | conflict | Well formed, but the diary refuses it: a taken time, working hours, a block, an already-cancelled booking. | Read details.reason. Offer another time; resend with force: true only when details.forceable is true and the owner decided to. |
| 422 | invalid_request | The body or query failed validation, or the ids are real but do not go together. | Read details — it names the field, and for a refused write the reason. Retrying unchanged will fail again. |
| 429 | rate_limited | More than 120 requests in a minute on this key. | Back off and retry after the seconds in Retry-After. |
| 429 | quota_exceeded | The key has spent its daily quota. | It resets at 00:00 UTC. Retry-After says how long that is. |
| 500 | internal_error | Something went wrong on our side. | Safe to retry with exponential backoff. |
401 and 403 are not interchangeable
A 401 means the credential was not accepted — a different key might work. A 403 means the credential is fine and the permission is missing; retrying it is a loop. Revoked, expired and unknown keys all return the same 401 message, because distinguishing them would confirm that a given key exists.
Why a write was refused
Creating, moving or cancelling a booking can be refused for many reasons, and they do not all want the same response. The status says what kind of refusal it is; details.reason names the exact case; details.forceable says whether resending with force: true is an answer. Nothing is written on any of them.
{
"error": {
"code": "conflict",
"message": "That time cannot be booked: Already booked at this time (appointment #1100)",
"details": {
"reason": "staff_busy",
"forceable": true,
"parameter": "startsAt",
"warnings": [
{
"kind": "staff_busy",
"message": "Already booked at this time (appointment #1100)",
"conflictingAppointmentId": 1100
}
]
}
},
"sandbox": "Sandbox data. These are fixtures for a made-up business: nothing here is a real booking, and nothing you send is stored."
}| Reason | Status | force | What it means |
|---|---|---|---|
service_not_found | 404 | no | No service with that id on this account. |
staff_not_found | 404 | no | No team member with that id on this account. |
location_not_found | 404 | no | No location with that id on this account. |
customer_not_found | 404 | no | No client with that id on this account. |
booking_not_found | 404 | no | No booking with that id on this account. |
service_inactive | 422 | no | The service is switched off, so it cannot be booked. |
staff_inactive | 422 | no | The team member is switched off, so they cannot be booked. |
location_inactive | 422 | no | The location is switched off, so nothing can be booked there. |
service_not_at_location | 422 | no | The service is not offered at that location. |
staff_cannot_perform_service | 422 | no | The team member does not perform that service at that location. details.capableStaff lists who does. |
starts_in_past | 422 | overrides it | startsAt has already passed. Usually a timezone mistake; force records a visit that already happened. |
multi_service_visit | 422 | no | The booking is one part of a visit with several services, which this endpoint cannot move. |
uses_pass | 422 | no | The booking was paid with a pass session, which this endpoint cannot move. |
staff_busy | 409 | overrides it | The team member already has a booking then. A one-to-one service is still refused with force (double_booked). |
buffer | 409 | overrides it | The time falls inside the buffer around another booking. |
outside_hours | 409 | overrides it | The time is outside the team member’s working hours, or on a day off. |
blocked_time | 409 | overrides it | The team member has blocked that time. |
teaching_class | 409 | overrides it | The team member is teaching a class then. |
calendar_busy | 409 | overrides it | The team member’s connected calendar shows them busy then. |
fully_booked | 409 | overrides it | Every place at that time for this group service is taken. |
double_booked | 409 | no | A one-to-one booking already holds that time. Nothing overrides this. |
room_unavailable | 409 | no | The room this service needs is taken or full then. Nothing overrides this. |
status_refused | 409 | no | The booking’s current status does not allow that change (already cancelled, or not confirmed). |
key_owner_missing | 409 | no | The user who created this key no longer exists, so a write cannot be attributed. Create a new key. |
write_failed | 409 | no | The write was refused for a reason this list does not name. Nothing was written. |
plan_limit | 403 | no | The plan’s booking allowance is used up. |
force: true is the owner’s own “Book anyway”: it overrides a busy time, a buffer, working hours, a block, a class, a connected calendar, a full group or a start in the past. It never overrides whose record something is, a one-to-one double-booking or a taken room. When a time has several problems, details.warnings lists them all.
Rate limits
Two limits apply to every key, and they answer different questions.
- 120 requests per minute. Burst control — it stops a runaway loop spending a whole day’s budget before anyone can revoke the key. Exceeding it returns
rate_limited. - A daily quota per key, set by the plan and reported by
GET /me. It resets at 00:00 UTC. Exceeding it returnsquota_exceeded.
The quota is counted per key, not per business, so a key you are debugging cannot starve the automation already running the business — and a key that misbehaves can be identified and revoked on its own.
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9997
Retry-After: 43200The first two are on successes as well as failures, so a client can slow itself down before it is refused rather than discovering the limit by hitting it. Retry-After is in seconds and appears only on a 429.
Paging
GET /customers takes limit and offset; GET /bookings is bounded by its date range instead. The default page is 50 and the maximum is 100; a larger limit is capped rather than rejected, because refusing teaches an integration nothing it can act on.
{
"data": [
{
"id": 12,
"name": "Priya Menon"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"hasMore": true
}
}There is no total. hasMore is resolved by fetching one row more than the page, not by a second count query over the same predicate — so paging stays cheap however deep you go.
Retrying safely
GETrequests are safe to retry.POST /bookingsis not idempotent — a retry after a timeout can create a second booking, so read the range back before retrying a create.- Cancelling an already-cancelled booking is refused rather than silently succeeding, so it is safe to call twice. Rescheduling to the time a booking already has answers
changed: falseand writes nothing, so it is safe to repeat too. - Back off exponentially on
429andinternal_error. Do not retry403,404orinvalid_requestunchanged.