Skip to content

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": {
"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.

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.

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

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.

When a create, a modify or a hold breaks the restaurant’s booking rules, the answer is always the same shape:

  • top-level code is always booking_rules_violated,
  • violations carries 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.

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

violations lists every broken rule among the checks that ran, and three checks sit outside that group:

  • no_service_at_this_time is 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_grid is 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_available and no_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.

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.

Terminal window
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-Key you 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. Putting date_in_past in override refuses the whole request with bad_request and param: "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 override on a booking that broke no rule costs nothing and is not logged against the booking.

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_error · 401 Unauthorized. No Authorization: Bearer … header was sent. Add the header — see Authentication.

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.

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.

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:

  • param absent — the key cannot call this endpoint. Ask the restaurant for a key with the scope named in the message. Retrying changes nothing.
  • param present — the key can call the endpoint, and only the field named in param is out of reach. Drop that field and re-send.

See Scopes asked for by one field for the four fields that do this.

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 — param is body,
  • a start_time that is not a time of day, such as 8pm or 25:00 — param is start_time,
  • a guest.phone that no carrier could route — param is guest.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.

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.

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.

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

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.

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.

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.

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.

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.

invalid_request_error · 422 Unprocessable Content. The umbrella on every booking-rule refusal. Read violations; see Booking-rule refusals.

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.

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.

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.

Overridable. Below the restaurant’s minimum. min_party_size is on GET /v1/configuration. param is party_size.

Overridable. Above the restaurant’s maximum. max_party_size is on GET /v1/configuration. param is party_size.

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.

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.

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.

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.

Not overridable. The date is before today for the restaurant. A service that has already happened cannot be sold; send a different date.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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/configuration publishes public_booking_url, and POST /v1/reservations?expand=modification_token returns 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.

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.

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.

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.

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.

api_error · 500. An unexpected error on the Service side. Retry with exponential backoff, and tell us if it persists.

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.

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.

  • Branch on code, fall back to type, never match on message.
  • On 422 booking_rules_violated, read violations and branch per entry on overridable. The top-level code tells you nothing else.
  • Retry 429 after Retry-After, and 500 / 502 with exponential backoff — on a write, always with the same Idempotency-Key.
  • Retry 409 idempotency_request_in_progress with the same key. Every other 409 needs you to re-read the resource first.
  • A 2xx carrying Idempotent-Replayed: true is an earlier attempt’s answer, not a new write. See Idempotency.
  • Never retry 400, 401, 403 or 404 unchanged. Each needs a different request, a different key, or a change by the restaurant.

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.

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.