Skip to content

Guides

Errors and limits

Every failure has the same shape and a stable code. Branch on the code, never on the prose.

Every error
{
  "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

StatusCodeWhat it meansWhat to do
401unauthorizedNo key, or one that is unknown, revoked or expired.Do not retry with the same key. Check it, or mint a new one.
403forbiddenThe 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.
403plan_requiredThe 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.
404not_foundNo 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.
405method_not_allowedA real endpoint, called with a method it does not take.The Allow header names the methods it does take.
409conflictWell 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.
422invalid_requestThe 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.
429rate_limitedMore than 120 requests in a minute on this key.Back off and retry after the seconds in Retry-After.
429quota_exceededThe key has spent its daily quota.It resets at 00:00 UTC. Retry-After says how long that is.
500internal_errorSomething 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.

A refused create
{
  "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."
}
ReasonStatusforceWhat it means
service_not_found404noNo service with that id on this account.
staff_not_found404noNo team member with that id on this account.
location_not_found404noNo location with that id on this account.
customer_not_found404noNo client with that id on this account.
booking_not_found404noNo booking with that id on this account.
service_inactive422noThe service is switched off, so it cannot be booked.
staff_inactive422noThe team member is switched off, so they cannot be booked.
location_inactive422noThe location is switched off, so nothing can be booked there.
service_not_at_location422noThe service is not offered at that location.
staff_cannot_perform_service422noThe team member does not perform that service at that location. details.capableStaff lists who does.
starts_in_past422overrides itstartsAt has already passed. Usually a timezone mistake; force records a visit that already happened.
multi_service_visit422noThe booking is one part of a visit with several services, which this endpoint cannot move.
uses_pass422noThe booking was paid with a pass session, which this endpoint cannot move.
staff_busy409overrides itThe team member already has a booking then. A one-to-one service is still refused with force (double_booked).
buffer409overrides itThe time falls inside the buffer around another booking.
outside_hours409overrides itThe time is outside the team member’s working hours, or on a day off.
blocked_time409overrides itThe team member has blocked that time.
teaching_class409overrides itThe team member is teaching a class then.
calendar_busy409overrides itThe team member’s connected calendar shows them busy then.
fully_booked409overrides itEvery place at that time for this group service is taken.
double_booked409noA one-to-one booking already holds that time. Nothing overrides this.
room_unavailable409noThe room this service needs is taken or full then. Nothing overrides this.
status_refused409noThe booking’s current status does not allow that change (already cancelled, or not confirmed).
key_owner_missing409noThe user who created this key no longer exists, so a write cannot be attributed. Create a new key.
write_failed409noThe write was refused for a reason this list does not name. Nothing was written.
plan_limit403noThe 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 returns quota_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.

Headers on every authenticated response
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9997
Retry-After: 43200

The 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.

A paged response
{
  "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

  • GET requests are safe to retry. POST /bookings is 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: false and writes nothing, so it is safe to repeat too.
  • Back off exponentially on 429 and internal_error. Do not retry 403, 404 or invalid_request unchanged.