Errors
The API uses conventional HTTP status codes and returns a structured error envelope on every failure, so you can branch on machine-readable fields rather than parsing prose.
Error envelope
Section titled “Error envelope”{ "error": { "type": "invalid_request_error", "code": "bad_request", "message": "limit must be a positive integer.", "param": "limit", "doc_url": "https://docs.useservice.app/api/errors#bad_request" }}| Field | Description |
|---|---|
type |
Broad category of the error (see below). Always present. |
code |
A specific, stable machine code for the failure. Always present. |
message |
A human-readable explanation, in English whatever language the restaurant works in. For logging and debugging — do not show it directly to guests, and do not match on it. |
param |
The offending request parameter — present only when a specific parameter caused the error. Omitted otherwise (it is not sent as null). |
doc_url |
A deep link to the entry for this code on this page (https://docs.useservice.app/api/errors#<code>). Always present. |
violations |
Present only on a booking-rule refusal. One entry per broken rule the checks reached — see What one refusal cannot tell you. |
Every response — successes and errors alike — also echoes the
Service-Version header that served it.
Error types
Section titled “Error types”type |
Meaning |
|---|---|
invalid_request_error |
The request was malformed, refused by a rule, or aimed at something that is not there. Covers 400, 403, 404, 409 and 422. |
authentication_error |
The API key is missing, malformed, or not valid (401). |
rate_limit_error |
You have exceeded the rate limit (429). |
api_error |
Something went wrong on the Service side, or at a provider Service depends on (500, 502). |
type is derived from the status, so it is the coarse branch. code is the one
to write logic against.
Scopes
Section titled “Scopes”Every API key carries an explicit list of scopes, chosen by the restaurant when
the key is minted. Scopes are flat: a broader-looking scope never implies a
narrower one, and a key created before scopes existed carries reservations:read
and nothing else. A key cannot widen its own scopes after creation — the
restaurant mints a new key.
A call that needs a scope the key does not carry is refused with
403 insufficient_scope, never with a silently reduced response. Guest data
inside a response you may read is the one exception: a booking’s
contact_email, contact_phone, guest_notes and internal_notes, and a
waitlist entry’s notes, are left out without guests:read, not refused. See
Guest data on a booking.
| Scope | Grants |
|---|---|
reservations:read |
Reading reservations, their events, availability, configuration, sections, holds, and the waitlist queue. |
reservations:write |
Creating, modifying and cancelling reservations; creating and releasing holds. There is no separate holds scope — a key that may book may hold. |
reservations:write:override |
Three fields. Sending the override array, which waives a booking rule the restaurant set. Setting excluded_from_shift_limits: true, which keeps a booking off the service’s covers caps permanently. Sending entire_section_id on a create, which books every table in a section, including one the restaurant keeps off online booking. Separate from reservations:write so an ordinary integration cannot do any of the three by accident. |
guests:read |
Reading guest records: GET /v1/guests, the guest value of expand, and its modification_token value on POST /v1/reservations. Also the guest data on every booking — the ones the key made included — and on every waitlist entry. The token is the guest’s own key to their booking page, card step included: see The guest token is the guest’s own key. |
Which scope each endpoint needs
Section titled “Which scope each endpoint needs”| Endpoint | Scope |
|---|---|
GET /v1/availability, GET /v1/availability/months |
reservations:read |
GET /v1/configuration |
reservations:read |
GET /v1/sections |
reservations:read |
GET /v1/reservations, GET /v1/reservations/{id}, GET /v1/reservations/{id}/events |
reservations:read |
POST /v1/reservations, PATCH /v1/reservations/{id}, POST /v1/reservations/{id}/cancel |
reservations:write |
GET /v1/holds, GET /v1/holds/{id} |
reservations:read |
POST /v1/holds, DELETE /v1/holds/{id} |
reservations:write |
GET /v1/waitlist-entries, GET /v1/waitlist-entries/{id} |
reservations:read |
GET /v1/guests, GET /v1/guests/{id} |
guests:read |
A waitlist entry is demand for a table and carries no guest record of its own, so
the queue is readable with reservations:read. The guest inside it is not: reach
it with expand=guest and the answer needs guests:read, wherever you reached it
from. The guest’s own request in the entry’s notes needs it too, and is absent
without it.
Scopes asked for by one field
Section titled “Scopes asked for by one field”Two scopes are demanded by something in the request rather than by the endpoint,
across four fields, and the refusal names that field in param:
| Field | Scope | param |
|---|---|---|
override: [...] on a write |
reservations:write:override |
override |
excluded_from_shift_limits: true |
reservations:write:override |
excluded_from_shift_limits |
entire_section_id on a create |
reservations:write:override |
entire_section_id |
expand=guest, and expand=modification_token on POST /v1/reservations |
guests:read |
expand |
One scope covers the first three, and each is still refused on its own: the
param tells you which field the key was turned away for, not which scope it is
missing. Holding reservations:write:override grants all three at once.
This is the whole reason insufficient_scope carries param. With no param,
your key cannot call the endpoint at all and re-sending is pointless. With a
param, your key can call it and only that one field is out of reach — dropping
the field and re-sending is a retry worth automating.
Sending override: [] or excluded_from_shift_limits: false asks for nothing, so
neither needs the scope. Only the assertion does. On a whole-section booking,
excluded_from_shift_limits: true is the default, so sending it is refused only
where entire_section_id already would be.
The entire_section_id check runs before the section is looked up, so a key
without the scope gets the 403 whether or not the id exists.
Booking-rule refusals
Section titled “Booking-rule refusals”When a create, a modify or a hold breaks the restaurant’s booking rules, the answer is always the same shape:
- top-level
codeis alwaysbooking_rules_violated, violationscarries one entry per rule broken,- the array is present even when exactly one rule was broken.
{ "error": { "type": "invalid_request_error", "code": "booking_rules_violated", "message": "The booking breaks one or more of this restaurant's booking rules. See `violations` for each one, and whether it can be overridden.", "doc_url": "https://docs.useservice.app/api/errors#booking_rules_violated", "violations": [ { "code": "party_size_too_large", "message": "Party size exceeds the maximum", "param": "party_size", "overridable": true }, { "code": "cutoff_passed", "message": "Reservation cutoff has passed", "overridable": true } ] }}The top-level code never names a rule, and the top-level message is the same
sentence for one violation and for six. Read the array. A client that reads the
top-level code instead works until the day a booking breaks two rules, and then
echoes back one of them and is refused by the one it never read.
Reading a violation
Section titled “Reading a violation”| Field | Description |
|---|---|
code |
The rule that was broken. Unique within one response — the same code never appears twice. |
message |
English, for your logs. One fixed sentence per code, with no figures in it: the limit that was crossed is on GET /v1/configuration (min_party_size, max_party_size, max_advance_days) or on the shift in GET /v1/availability (effective_cutoff_minutes). The one exception is start_time_off_grid, whose message names the time you sent and the service’s seating times. |
param |
The request field to change, when one field is at fault. Omitted otherwise. |
overridable |
Whether this violation can be waived by re-submitting with the code in override. Always present on every entry. |
conflicts |
Present only on section_occupied: one entry per occupied table, as table, start_time and end_time. |
Violations carry no doc_url of their own. Build one from the code, the same way
the envelope does: https://docs.useservice.app/api/errors#<violation code>.
What one refusal cannot tell you
Section titled “What one refusal cannot tell you”violations lists every broken rule among the checks that ran, and three checks
sit outside that group:
no_service_at_this_timeis checked first. Every other rule is judged against a service, so when no service covers the date and time, nothing else is checked and this violation arrives alone.start_time_off_gridis checked next. A service covers the time and does not seat at that minute. Every other rule is judged against a seating, so this violation arrives alone too.- The table search runs last, once every rule has passed or been
overridden. Its refusals,
no_table_availableandno_table_available_in_section, are never listed next to the rules they follow.
So a booking refused for party_size_too_large and re-submitted with that code
in override can come back with a second 422, this time naming
no_table_available. The override did its job; the restaurant has no table
that seats the party at that time. A desk screen that offers “override and
book” has to handle that second answer, and tell the operator the booking
cannot be seated rather than offering another override.
Overriding a rule
Section titled “Overriding a rule”overridable: true means a key holding reservations:write:override can
re-submit the identical booking with that code in an override array, and the
rule is waived rather than enforced. Which codes are waivable is fixed in the API,
not per restaurant.
curl -X POST "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "service_date": "2026-10-04", "start_time": "20:00", "party_size": 9, "guest": { "first_name": "Marie", "last_name": "Dupont" }, "override": ["party_size_too_large"] }'Four things decide whether that call works:
- Retry once, not in a loop. Collect the overridable codes from
violations, decide, re-submit once. A second refusal is a different booking problem, not a longer list — typically the table search, which the first refusal cannot report. - Send the same
Idempotency-Keyyou sent the first time. The refused attempt created nothing, and the key is what stops a timeout from double-booking. - A code that is not overridable is a
400, not a silent ignore. Puttingdate_in_pastinoverriderefuses the whole request withbad_requestandparam: "override", and the message says whether the code is a real rule that can never be waived or not a rule at all. - You can send the array pre-emptively. Overrides are recorded only when they
actually waived something, so an
overrideon a booking that broke no rule costs nothing and is not logged against the booking.
No service, versus a service that is full
Section titled “No service, versus a service that is full”Three refusals sit close enough together to be confused, so they carry
different codes. On a write, all three arrive inside violations, under the
same umbrella.
| Code | What happened | overridable |
param |
|---|---|---|---|
no_service_at_this_time |
Nothing serves that date and time. The restaurant is closed then, or the hour falls between two services. | false |
absent |
start_time_off_grid |
A service is running and does not seat at that minute: the time falls between two seatings, or after the last one. | false |
start_time |
slot_no_longer_available |
A service is running and its covers cap is full. | true |
party_size |
The remedies are different, which is why the codes are. For
no_service_at_this_time and start_time_off_grid, re-read
GET /v1/availability and send a time it publishes — nothing else makes that
booking work, and naming either code in override is a 400. For slot_no_longer_available, send a
smaller party, a different time, or the same booking with the code in override
if your key carries the scope.
overridable still answers the question you act on, and it is on every violation
for that reason. The codes differ so that you can also log, count and alert on
the two separately — a restaurant that is closed at the time your system keeps
asking for is a different problem from one that keeps selling out.
Authentication and access
Section titled “Authentication and access”auth_header_missing
Section titled “auth_header_missing”authentication_error · 401 Unauthorized. No Authorization: Bearer … header
was sent. Add the header — see Authentication.
invalid_token
Section titled “invalid_token”authentication_error · 401 Unauthorized. A key was supplied but is invalid,
revoked, expired, or unknown.
Create a new key if yours no longer
works. Re-sending the same key cannot succeed.
plan_required
Section titled “plan_required”invalid_request_error · 403 Forbidden. The key is valid and the restaurant
exists, but that restaurant’s subscription does not include API access. This is a
billing state, not a permissions one, so no amount of re-scoping the key will
clear it — and neither will a new key. Ask the restaurant to upgrade; access
returns on the next request once they have, with no change on your side.
insufficient_scope
Section titled “insufficient_scope”invalid_request_error · 403 Forbidden. The key is valid and the restaurant is
entitled; the key does not carry the scope this call needs. message names the
missing scope.
Read param before you react:
paramabsent — the key cannot call this endpoint. Ask the restaurant for a key with the scope named in the message. Retrying changes nothing.parampresent — the key can call the endpoint, and only the field named inparamis out of reach. Drop that field and re-send.
See Scopes asked for by one field for the four fields that do this.
Request shape
Section titled “Request shape”bad_request
Section titled “bad_request”invalid_request_error · 400 Bad Request. A parameter, filter, cursor, header
or body field is malformed, unknown, or missing. param names it. Common causes:
an unknown expand value, a malformed date filter, a locale the restaurant
does not publish, a section_id or entire_section_id that is not a sec_… id,
entire_section_id sent with section_id or hold_id, a duration other than
until_shift_end or sent without entire_section_id, a metadata value that
is not an object, an override entry that is not an overridable code, an
excluded_from_shift_limits value that is not a JSON boolean, a Service-Version
that is not supported (param is the header’s name, Service-Version), and a
PATCH body containing no modifiable field.
The list filters refuse two more by name. service_date on GET /v1/reservations
or GET /v1/holds is a 400 with param: "service_date": the filter is date,
or the range date[gte] and date[lte]. A status on GET /v1/reservations
outside the published enum is a 400 too, and late is not in it.
A write body is read strictly. A field the endpoint does not read is a 400
naming it in param, rather than a field silently ignored. Inside guest the
name is dotted (guest.vip), and a body that is a JSON array rather than an
object gets param: "body". The keys inside metadata are never checked.
Write fields go in the JSON body. One sent in the query string instead —
PATCH /v1/reservations/{id}?party_size=6 — is a 400 naming it in param,
even when the body is empty, rather than a change applied from the URL. Query
parameters that are not write fields, such as expand, are read as before.
Three more come from the body’s content:
- a body that is not valid JSON —
paramisbody, - a
start_timethat is not a time of day, such as8pmor25:00—paramisstart_time, - a
guest.phonethat no carrier could route —paramisguest.phone. A number without a country code is read in the restaurant’s country.
Fix the request. Retrying it unchanged fails the same way.
PATCH /v1/reservations/{id} also answers bad_request when the body carries a
field that is fixed at creation: guest, guest_id, guest_notifications,
metadata, hold_id, entire_section_id and duration — or guarantee_consent_amount_cents, which belongs to
the guest’s own booking page and cannot be sent on their behalf (see
guarantee_consent_guest_only). param names
the field and message says why. These are refused rather than ignored, so a
partner that sends guest_notifications on a PATCH never gets a 200 that
implies the setting changed.
host_metadata_too_large
Section titled “host_metadata_too_large”invalid_request_error · 422 Unprocessable Content. The metadata object on
POST /v1/reservations or POST /v1/holds has more than 50 keys, or serializes
to more than 4,096 bytes. The code’s name comes from the booking widget, which
shares this limit. Send less: keep a reference to your own record in metadata,
not the record.
validation_error
Section titled “validation_error”invalid_request_error · 422 Unprocessable Content. A value was well-formed but
the record refused it — the message carries the record’s own complaint. Read the
message, fix the value.
guest_fields_required
Section titled “guest_fields_required”invalid_request_error · 422 Unprocessable Content. The booking carries no
first_name or no last_name. Both are required on a create that carries a
guest object — a party with nobody’s name on it cannot be greeted at the door.
A create that identifies an existing profile with guest_id is exempt. The
names are already on that profile, and re-sending them would make guest_id
useless for the integration it exists for. required_guest_fields on
GET /v1/configuration describes the same rule.
require_guest_email and require_guest_phone are published on
GET /v1/configuration as advice about the restaurant’s own booking form.
They do not bind this API, so email and phone are optional here — with one
exception, which is a slot carrying a card guarantee. See
email_required_for_guarantee.
Idempotency
Section titled “Idempotency”Idempotency-Key is required on POST /v1/reservations and
POST /v1/holds, and optional on PATCH /v1/reservations/{id} and
POST /v1/reservations/{id}/cancel. Send a unique value per attempt, and the same
value when you retry that attempt. Keys are remembered for 72 hours.
A retry of a request that already completed is answered from that request: the
original status and body, verbatim, the Location header when the original
carried one, and the header Idempotent-Replayed: true.
A first write never carries the header. It is how you tell “this call created
the booking” from “an earlier call did” before you act on a 201.
The body is the one recorded the first time, including any expand the first
request asked for, whatever the retry asks for. That does not let a key read more
than its scopes allow: the fingerprint includes the API key that sent the
request, so no other key can replay it, and a key’s scopes are fixed when it is
created.
idempotency_key_required
Section titled “idempotency_key_required”invalid_request_error · 400 Bad Request. A write that requires the header was
sent without it. This is a 400 rather than a 422 because the booking was never
interpreted: add the header and send the request again unchanged.
idempotency_key_invalid
Section titled “idempotency_key_invalid”invalid_request_error · 400 Bad Request. The header was present but is longer
than the maximum length, or contains control characters. Send a printable string
— a UUID per attempt is the usual choice.
idempotency_request_in_progress
Section titled “idempotency_request_in_progress”invalid_request_error · 409 Conflict. An earlier request with this key is
still running. Wait a moment and retry with the same key. Minting a new key
here is how you double-book.
idempotency_key_reuse
Section titled “idempotency_key_reuse”invalid_request_error · 409 Conflict. This key was already used for a
different request body. The key is the fingerprint of one attempt, so reusing
it for another booking is refused rather than answered with the first booking.
Mint a new key for the new request.
Two keys generated independently can collide on a string: keys are namespaced per restaurant, not per API key, so another integration on the same restaurant can consume a key string you would have used. Random per-attempt values avoid this.
Booking rules
Section titled “Booking rules”On POST /v1/reservations, PATCH /v1/reservations/{id} and POST /v1/holds,
every code in this section arrives inside violations, under the
booking_rules_violated umbrella, and never as the top-level code.
GET /v1/availability is the one exception. It checks four of these rules
before it computes a slot, and answers a failure with 422 and the rule as the
top-level code, with no violations array and no param:
date_in_past,
party_size_too_small,
party_size_too_large and
advance_window_exceeded. A read books nothing, so
there is nothing to override: change the query. GET /v1/availability/months
answers none of them: it takes no party size, and it flags a past day or a day
outside the window with past or outside_window instead of refusing.
booking_rules_violated
Section titled “booking_rules_violated”invalid_request_error · 422 Unprocessable Content. The umbrella on every
booking-rule refusal. Read violations; see
Booking-rule refusals.
slot_no_longer_available
Section titled “slot_no_longer_available”Overridable. A service covers that time and is full: its covers cap has no
room left for a party this size. Send a smaller party, a different time, or the
same booking with this code in override if your key holds the scope. param is
party_size.
It does not mean the restaurant is shut then — that is
no_service_at_this_time. See
No service, versus a service that is full.
cutoff_passed
Section titled “cutoff_passed”Overridable. Online booking for that service has closed, some number of
minutes before it starts. The cutoff is per service, and the one actually enforced
is published as effective_cutoff_minutes on each shift in
GET /v1/availability — read it there rather than from the restaurant-level
default, which a service can override.
advance_window_exceeded
Section titled “advance_window_exceeded”Overridable. The date is further out than the restaurant takes online
bookings. max_advance_days on GET /v1/configuration carries the window, so you
can refuse the date before you send it.
party_size_too_small
Section titled “party_size_too_small”Overridable. Below the restaurant’s minimum. min_party_size is on
GET /v1/configuration. param is party_size.
party_size_too_large
Section titled “party_size_too_large”Overridable. Above the restaurant’s maximum. max_party_size is on
GET /v1/configuration. param is party_size.
slot_blocked
Section titled “slot_blocked”Overridable. Staff marked that specific slot or service full from the back office. The table may be physically free, which is why this is waivable at all — the restaurant stopped selling the slot rather than running out of room.
shift_not_online_bookable
Section titled “shift_not_online_bookable”Overridable. The service is open and visible, and takes no online bookings
(its online mode is display-only). Availability publishes these services with
display_only set, so you can render them as information rather than offering
them.
no_service_at_this_time
Section titled “no_service_at_this_time”Not overridable. Neither a service nor a date-specific override covers that
date and time, so there is nothing to book — the restaurant is closed then, or
that hour falls between two services. GET /v1/availability publishes the times
that exist; read it and send one of those.
Naming this code in override is a 400, not a silent no-op.
There is no capacity judgement here to waive: the booking has no service to
belong to, and no key and no scope changes that.
start_time_off_grid
Section titled “start_time_off_grid”Not overridable. A service is running at that time and does not seat at that
minute: the time falls between two of its seatings, or after its last one.
param is start_time. The message names the time you sent and the service’s
seating times, for example
20:07 is not one of this service's seating times: it seats every 30 minutes from 19:00 to 21:00.
Send a start_time that GET /v1/availability offers for that service.
It is checked on a create, on a hold, and on a PATCH that changes the date or
the time. A PATCH that keeps both is not checked, because the restaurant’s
staff can place a booking on any minute. Converting a hold is not checked
either: the hold already secured the seat. Like
no_service_at_this_time, it arrives alone, and
naming it in override is a 400.
date_in_past
Section titled “date_in_past”Not overridable. The date is before today for the restaurant. A service that has already happened cannot be sold; send a different date.
slot_elapsed
Section titled “slot_elapsed”Not overridable. The date is valid and that time of day has already passed.
Distinct from date_in_past because the remedy is different: a later time on the
same day, rather than a different day. param is start_time.
no_table_available
Section titled “no_table_available”Not overridable. The covers were available and no single table or combination fits the party at that time. The override mechanism does not reach this one: every way of honouring it would produce a booking seated somewhere nobody chose, or a booking with no table at all. Try a different time, or a different party size.
Naming this code in override is a 400 saying it is a real
rule that can never be waived — not a 400 saying it is not a code. The covers
judgement you can waive is slot_no_longer_available,
and this refusal means the covers were there and no table fits.
no_table_available_in_section
Section titled “no_table_available_in_section”Not overridable. Same refusal, narrowed by the section_id you asked for: no
table this service serves is in that section. Drop section_id and re-submit to
let the restaurant seat the party anywhere, or pick another section from
GET /v1/sections.
On a whole-section booking the same code, with param: "entire_section_id",
means the section is not drawn on the floor plan in use for that service. Try a
different date or a different section.
section_occupied
Section titled “section_occupied”Not overridable. A whole-section booking (entire_section_id) was asked for
and part of the section is already taken during the window the booking would
hold: a booking on one of its tables, or another whole-section booking of it.
Nothing is placed; a whole-section booking is never placed on part of a room.
param is entire_section_id.
The violation carries conflicts, one entry per occupied table:
| Field | Description |
|---|---|
table |
The occupied table’s tbl_… id. null when the section has no tables and another whole-section booking holds it. |
start_time |
HH:MM, when the table is held from. |
end_time |
HH:MM, when it is held to. null for a booking stored without an end time. |
Entries are ordered by table, then start time. They name nothing about who holds
the table. Try a different time or date, or ask the restaurant to move the
parties listed. Naming this code in override is a 400. See
Book a whole room.
duplicate_booking
Section titled “duplicate_booking”Overridable. The same guest already holds a live booking at the same slot. Idempotency does not catch this: it catches the same request twice, not two different requests for one guest — a guest who books once through the widget and once through you, or a retry whose key was regenerated. One person legitimately hosting two tables exists, so this is a warning and not a wall.
guarantee_required
Section titled “guarantee_required”Overridable. The slot carries a card guarantee, and it cannot be completed because no email address will reach the guest. Two remedies: send a guest email address, or override, which books without the guarantee. Override this deliberately — waiving it is the whole point when a booking is taken by phone, and the restaurant then holds no card for a no-show.
This one can also be sent pre-emptively, before ever seeing the refusal.
guest_notifications_required_for_guarantee
Section titled “guest_notifications_required_for_guarantee”Not overridable. You sent guest_notifications: "partner_managed" for a slot
that carries a card guarantee. The guarantee is an email: the guest is sent a
pay-link, a reminder, and a receipt, and without them the card is never saved, the
hold lapses and the booking auto-cancels — after you have already told the guest
the table is theirs.
Two remedies, both yours: drop the suppression for this booking, or send
override: ["guarantee_required"], which books without the guarantee and leaves
no pay-link to suppress.
widget_disabled
Section titled “widget_disabled”Not overridable. The restaurant’s account no longer permits online bookings at
all. You should not reach this code, because the same condition fails
authentication as plan_required — seeing it means the account’s
state changed between authenticating and writing. Nothing on your side fixes it.
Reservation and hold state
Section titled “Reservation and hold state”not_found
Section titled “not_found”invalid_request_error · 404 Not Found. No such resource for this key’s
restaurant, or no such endpoint. See
404, not 403, across restaurants.
When the id that matched nothing was sent in the body or the query string —
guest_id, hold_id, section_id, entire_section_id, company_id or
excluding — param names that field.
A hold id that has already been converted or released — by you, or by the
restaurant from its back office — also answers 404 on
POST /v1/reservations { hold_id }: from the conversion’s point of view there is
no usable hold, which is what stops the same hold being spent twice.
reservation_finalized
Section titled “reservation_finalized”invalid_request_error · 409 Conflict. The booking’s life is over — it is
completed or no_show, or (on PATCH only) already cancelled. Nothing you
send will change it. Read status and stop writing.
Cancelling an already-cancelled booking is not this: that answers 200 with
the same terminal object, because the goal is already true.
hold_already_converted
Section titled “hold_already_converted”invalid_request_error · 409 Conflict. DELETE /v1/holds/{id} on a hold that
has already become a booking. There is nothing left to release, and the table is
still taken — by a reservation. The message names it; a hold and its reservation
share the id suffix, so hold_AbC became resv_AbC. Cancel that reservation if
you meant to give the table back.
This is deliberately a 409 and not the 404 the conversion path returns for the
same hold. DELETE asks “make sure this is not holding a table”, and a 404
would tell you the hold never existed while you are in fact holding a confirmed
booking.
section_booking_not_modifiable
Section titled “section_booking_not_modifiable”invalid_request_error · 409 Conflict. PATCH /v1/reservations/{id} on a
booking made with entire_section_id. Every change is refused, guest_notes
alone included, because a modify re-seats the party on one table or one
combination and would shrink the booking to part of the room. Two bodies are
answered before this check: an empty one, and one naming a field no PATCH
accepts, each get a 400 first. Cancel and rebook, or ask the restaurant
to change it from its back office. The guest’s own manage link is refused the
same way.
conflict
Section titled “conflict”invalid_request_error · 409 Conflict. A status transition this API did not
anticipate. Every shape you can reach deliberately has a more precise code, so
this is the backstop rather than a code to branch on. Re-read the reservation and
look at status.
Card guarantees
Section titled “Card guarantees”Some slots require a card imprint. A create on one of those slots produces a
booking in awaiting_guarantee and emails the guest a pay-link. Once the card is
saved the booking becomes confirmed, or pending when the service asks the
restaurant to approve its bookings. The codes below are the ways that can be
refused.
A card is asked for on two paths. The slot asks for one by itself, whatever you
send; or you send a guarantee object on a create or a PATCH and ask for one
on a slot that would take none. See Ask for a card
yourself.
The two paths part company when the restaurant’s card payments are not active.
The slot asks for nothing, no code here is returned, and the create is taken
without a guarantee — see
Does this venue ask for a card?
A guarantee you sent is refused instead, with
payments_suspended.
email_required_for_guarantee
Section titled “email_required_for_guarantee”invalid_request_error · 422 Unprocessable Content. The slot carries a card
guarantee and the booking has no email address to send the pay-link to. Send
guest.email, or reuse a guest_id whose profile already carries one.
This is the single place where a guest email is mandatory on this API. Everywhere else it is optional, which is why the code is specific rather than a generic missing-field error.
On a create that gets as far as the booking-rule checks, the same refusal
arrives inside
violations alongside guarantee_required, which offers
the other remedy. On a hold conversion it arrives flat, with no array.
guarantee_guest_email_required
Section titled “guarantee_guest_email_required”invalid_request_error · 422 Unprocessable Content. The booking reached the
guarantee step with no guest record, or with a guest record carrying no contact
email, so the pay-link has nowhere to go. Send a guest with an email address.
Distinct from email_required_for_guarantee,
which is checked earlier and reports the remedy that costs nothing. A create
with no address takes that earlier refusal, whichever path asked for the card,
and its message names the path. This code is what a guarantee on a PATCH
answers when the booking it names has nobody to email.
guarantee_amount_invalid
Section titled “guarantee_amount_invalid”invalid_request_error · 422 Unprocessable Content. The guarantee the
restaurant configured for this slot resolves to a non-positive amount, so there is
nothing to ask the guest for. A restaurant configuration problem, not a request
problem — report it to them, or send guarantee.amount_cents yourself and name
the figure.
Your own guarantee.amount_cents never produces this code: a non-positive or
fractional amount is refused at the door, as a 400 naming
guarantee.amount_cents. This code is reached only where the amount was left to
the restaurant’s own policy.
guarantee_request_not_allowed
Section titled “guarantee_request_not_allowed”invalid_request_error · 422 Unprocessable Content. The guarantee could not be
raised against the booking’s state.
On a guarantee you sent with a PATCH, that state is the booking’s: a card can
be asked for only on an upcoming booking that is confirmed and carries no
guarantee yet. A booking awaiting approval has to be approved first, and one that
already carries a card has to have it released from the back office. Re-read the
booking before you ask again.
On the create path it is an internal inconsistency rather than something your request chose; retry the create once, and report it if it repeats.
payments_suspended
Section titled “payments_suspended”invalid_request_error · 422 Unprocessable Content. You asked for a card with
guarantee, and the restaurant cannot take card payments — its payment setup is
unfinished, or its account has been restricted since. There is no way to collect
the card, so nothing is written.
The same restaurant answers a card slot differently: the booking is taken and confirmed with no guarantee, and you are told nothing, because nothing was refused. Asking explicitly is the one place a suspended account is an error.
guarantee_consent_guest_only
Section titled “guarantee_consent_guest_only”invalid_request_error · 422 Unprocessable Content. A PATCH would have raised
the guaranteed amount on a booking whose card was saved through the restaurant’s
own widget — growing the party is the usual way — and a higher amount is a higher
ceiling the restaurant may one day charge. Raising that ceiling needs the
cardholder’s agreement to the new figure, which is theirs to give and cannot be
given on their behalf. Neither your key nor the restaurant’s own staff can assert
it.
Nothing changed. The whole modify rolls back and the booking is exactly as it was, including any other field in the same body.
Three ways forward:
- Make a change that does not raise the amount. Moving the day or the time applies, with the free-cancellation deadline following the new seating, and so does shrinking the party — the imprint is re-priced downward without asking anyone, because the existing, higher mandate already covers the smaller figure.
- Ask the restaurant. Back-office staff can change the party size; the guarantee keeps its existing, lower ceiling.
- Send the guest to their own booking page, which is the one surface that can
take a renewed mandate.
GET /v1/configurationpublishespublic_booking_url, andPOST /v1/reservations?expand=modification_tokenreturns the token for a booking you created.
A booking your key created cannot reach this code. The card behind a widget
booking was entered by the guest, who agreed to an amount as they saved it, and
raising that amount is a new agreement only they can give. The card behind a
booking your key created was requested through the guest’s pay-link
(origin: "staff_request"), at the amount the restaurant’s rules set or the
amount you asked for. No figure was put in front of that guest to agree to, and
the card keeps the amount it was requested at for the life of the booking. See
Changing the booking before the card is
saved
and Ask for a card
yourself.
You still meet this code, because a sync hands you the bookings the restaurant took through its own widget, and your modify path runs over those too.
details.guarantee_amount_cents is not published in the envelope; the amount is
not actionable for you, since there is no figure you could agree to.
guarantee_modify_locked
Section titled “guarantee_modify_locked”invalid_request_error · 403 Forbidden. The booking carries an active card
imprint and its free-cancel deadline has passed, so it can no longer be modified —
that window is what stops a party shrinking from eight to two an hour before
service to dodge the imprint.
Cancelling stays available, with whatever fee the restaurant’s terms state. A
403 here is about the booking’s state, not your key: no scope changes it.
guarantee_modify_requires_card
Section titled “guarantee_modify_requires_card”invalid_request_error · 422 Unprocessable Content. A PATCH would put a
booking with no card onto a card guarantee: the party grows past the size at
which the restaurant asks for a card, or the booking moves to a service that asks
every booking for one. A PATCH has no step that takes a card, so it is refused
before anything is written. Nothing changed, including any other field in the
same body.
It arrives flat, not inside violations, and it is never waivable: naming it in
override is a 400.
Cancel and rebook. The create is the call that takes a card: the new booking
lands awaiting_guarantee and the guest is sent the pay-link. If your key carries
reservations:write:override, the create can instead waive
guarantee_required and book with no card held. A change
that brings no guarantee on applies normally.
The guest’s own manage link is no way round it. That page answers the same refusal: it tells the guest nothing was changed, and to cancel and make a new reservation or contact the venue. No modification takes a card, on either door.
A guarantee that was released, withdrawn or expired counts as no card. Two cases
are not refused: a booking whose current slot and party already call for a
guarantee it does not have, such as one created with guarantee_required waived,
and any booking at a restaurant whose card payments are not active at the time.
Transport and server
Section titled “Transport and server”rate_limited
Section titled “rate_limited”rate_limit_error · 429 Too Many Requests. Wait for the Retry-After
interval, then retry. Reads and writes draw on separate budgets and
RateLimit-Resource names the one you just spent — see
Rate limits.
payment_provider_error
Section titled “payment_provider_error”api_error · 502 Bad Gateway. The payment provider was unreachable or refused
while taking the imprint. The booking was not created. Retry with the same
Idempotency-Key, backing off.
internal_error
Section titled “internal_error”api_error · 500. An unexpected error on the Service side. Retry with
exponential backoff, and tell us if it persists.
HTTP status codes
Section titled “HTTP status codes”| Status | Meaning |
|---|---|
200 OK |
The request succeeded. |
201 Created |
A reservation or hold was created. |
304 Not Modified |
A conditional GET whose If-None-Match matched. See Expanding objects. |
400 Bad Request |
The request was never interpreted — a malformed or unknown input. Check param. |
401 Unauthorized |
Missing or invalid API key. See Authentication. |
403 Forbidden |
The key, the plan, or the booking’s state forbids this. Not fixable by retrying. |
404 Not Found |
No such resource for this key’s restaurant, or no such endpoint. |
409 Conflict |
The resource’s state refuses this, or an idempotency key is in flight or re-used. |
422 Unprocessable Content |
The request was understood and refused by a rule. Booking rules arrive here with violations. |
429 Too Many Requests |
Rate limited. See Rate limits. |
500 / 502 / 503 |
Something went wrong on the Service side or at a provider. Retry with exponential backoff. |
404, not 403, across restaurants
Section titled “404, not 403, across restaurants”If you request a resource that exists but belongs to another restaurant, the
API returns 404 Not Found, not 403 Forbidden. From your key’s point of
view the resource does not exist. This avoids leaking the existence of other
restaurants’ data. A valid id you expect to work but that returns 404 almost
always belongs to a different restaurant, or to a different key.
Handling errors
Section titled “Handling errors”- Branch on
code, fall back totype, never match onmessage. - On
422 booking_rules_violated, readviolationsand branch per entry onoverridable. The top-level code tells you nothing else. - Retry
429afterRetry-After, and500/502with exponential backoff — on a write, always with the sameIdempotency-Key. - Retry
409 idempotency_request_in_progresswith the same key. Every other409needs you to re-read the resource first. - A
2xxcarryingIdempotent-Replayed: trueis an earlier attempt’s answer, not a new write. See Idempotency. - Never retry
400,401,403or404unchanged. Each needs a different request, a different key, or a change by the restaurant.
Codes not on this page
Section titled “Codes not on this page”This page lists every code the Service API returns. Service’s back office and booking widget share the same error vocabulary and have codes of their own, for staff sessions, floor plans, waitlist sign-up and card administration. None of them reaches an API key, so they are not listed here.
A code from this API with no entry on this page is a bug. Report it with the request id and the full envelope.
Getting help
Section titled “Getting help”Every response carries an X-Request-Id header, on a success as on a
failure, and its value names that one request in Service’s logs. Log it next to
the envelope you received: it is the request id the paragraphs above ask you to
quote.
Report a problem through the Service contact page, quoting that id and the envelope in full.