Create a reservation
const url = 'https://api.useservice.app/v1/reservations';const options = { method: 'POST', headers: { 'Idempotency-Key': 'example', Authorization: 'Bearer <token>', 'Content-Type': 'application/json' }, body: '{"service_date":"2026-10-16","start_time":"19:30","party_size":2,"guest":{"first_name":"Camille","last_name":"Dubois","email":"[email protected]"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.useservice.app/v1/reservations \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: example' \ --data '{ "service_date": "2026-10-16", "start_time": "19:30", "party_size": 2, "guest": { "first_name": "Camille", "last_name": "Dubois", "email": "[email protected]" } }'Books a table, applying the same rules the restaurant’s own booking widget applies.
Requires the reservations:write scope. The restaurant is resolved from the API key —
there is no restaurant field.
Check status. A 201 does not mean confirmed. Under manual approval the booking
lands pending; on a slot carrying a card guarantee it lands awaiting_guarantee, the
guest is emailed a payment link, and the booking only becomes valid once the card is
saved. Only confirmed means the table is booked. The booking is retrievable from
GET /v1/reservations/{id} immediately either way — poll it, or watch for
reservation.updated, to see whether the guest completed the card step.
Idempotency-Key is REQUIRED. Send a unique value per booking attempt and the SAME value
when retrying it; the original response is then replayed verbatim.
A field this operation does not accept is a 400, with code: "bad_request" and
param naming it, dotted inside guest (guest.vip). Nothing is booked. That covers the
fields a desk tool reaches for — internal_notes, table_ids, company, status,
source — none of which this API writes. The keys inside metadata are yours and are never
checked. Fields go in the JSON body: one sent in the query string is a 400 naming it too.
Overrides. A booking refused by the restaurant’s rules is a 422 with
code: "booking_rules_violated", and violations lists every rule it broke, each entry
flagged overridable. That top-level code and message are the same for one broken rule as
for six, so read violations — never the top-level code — to learn what was refused.
There is no other shape: every refusal by a booking rule answers this way, including the
ones that can only ever break one rule, such as a start_time no service covers or a slot
with no table left. Only a violation flagged overridable: true can be waived; the rest
name a different remedy.
Re-submit with override set to the overridable codes you accept and the booking is made
anyway — per booking, never as a standing permission. Requires the
reservations:write:override scope; sending a non-empty override without it is a 403
with code: "insufficient_scope" and param: "override". That param is the difference
between a key that cannot use overrides — the same request without override would be
accepted — and a key that cannot reach this endpoint at all, which is the same code with
no param. Naming a code that can never be overridden is a 400. Accepted overrides are
recorded on the reservation’s created event as overrides_accepted.
Retry at most once. Between your 422 and your retry the slot is re-locked and re-evaluated, so a violation that was not in the list you were handed can appear — a loop that re-sends whatever codes come back widens what it accepts without ever deciding to. Retry once with the codes you meant to accept; if the second attempt still refuses, it is refusing for a reason you have not agreed to.
override also works without the round trip: a desk that always books past cutoff can
send override: ["cutoff_passed"] on every call and never see a 422. Only overrides that
actually waived something are recorded.
Owning guest communication. Send guest_notifications: "partner_managed" when you
send the guest their own confirmation — a packaged stay, say, where one message covers the
room, the show and the dinner. We then send that guest nothing at all about this
booking, for as long as it exists: no confirmation, no reminder, nothing when it is
modified or cancelled, and no satisfaction survey. That includes the modification link,
which only ever travels in those messages — so the guest has no self-service route and
changes are yours to make, through this API. The restaurant’s own staff alerts are
unaffected, and you still receive every reservation.* webhook, which is how you learn
about a change the restaurant made at the desk.
Two things it cannot do. It is refused with guest_notifications_required_for_guarantee
on a slot that carries a card guarantee: the guest is asked for the card by email, so
suppressing it would leave a booking that auto-cancels two days later after you have
already told the guest it was confirmed. Override guarantee_required instead if you mean
to book without the guarantee. And it never silences a payment message — a pay-link,
receipt or refund — if the restaurant attaches a guarantee to the booking afterwards.
It also stacks with the fact that guest contact details are optional here: a booking with
neither an email nor a phone AND partner_managed is one that only you can reach. That is
allowed on purpose: the restaurant cannot contact that guest about a closure.
The language the guest is written to in. Send guest.language. Leave it out and every
guest you create is written to in the restaurant’s own primary_language — the
confirmation, the reminder, the modification link, the survey. That is correct only where
the guest speaks the restaurant’s own language, and the mismatch is silent: nothing in the
201 says which language the e-mail went out in. It is stored on the guest profile and
snapshotted onto this booking, so a later change to the profile does not rewrite which
language this booking’s messages were sent in. An unsupported value is a 400 with
param: "guest.language", never a silent fallback.
⚠️ It is accepted platform-wide but honoured only within guest_languages. The field takes
any language the platform supports, because a guest’s language is a fact about the guest
rather than about the restaurant. What the guest is actually written to in, though, is the
first of guest.language → the restaurant’s primary_language that appears in the
restaurant’s guest_languages — so "language": "pt" at a restaurant publishing only fr
is stored on the profile and produces a French e-mail anyway. Read guest_languages from
GET /v1/configuration to know which languages this restaurant can really write in; sending
one outside it records the guest’s preference for the day the restaurant adds it, and
changes nothing today.
⚠️ This is separate from ?locale= on GET /v1/availability, which decides the language of
the text WE hand YOU — shift names, guest messages, closure text — for rendering in your own
funnel.
The guest’s own key to their booking page. Add ?expand=modification_token and the
response carries modification_token, the guest’s credential for their booking page. That
page is on the booking host public_booking_url in GET /v1/configuration names, not on this
API, and the link is {public_booking_url}/{modification_token}, the one our confirmation
e-mail sends. The link works either way; whether we send it follows guest_notifications.
Ours carries one extra parameter, ?locale=, naming the language that message was written
in, so the page opens in the language the guest was addressed in. Yours does not have to:
without it the page follows the guest’s browser, then the restaurant’s own language. Append
it yourself if you know better than we do.
On service_managed (the default) our confirmation carries it, so the guest has it without
you. On partner_managed we send no confirmation, and the link reaches the guest only if
you pass it on.
Whoever holds the token can do on that page everything the guest can, including the card
step, and the endpoints behind that page are the booking widget’s, not this API’s. So it is
gated on guests:read (a key without it gets a 403 insufficient_scope with
param: "expand"). Pass it only to the guest; never log it or display it. It is returned on
THIS response only (reads, lists and webhooks never carry it), and every booking has one, so
it is never null when present.
Private events that do not fill the room. A booking carrying
excluded_from_shift_limits: true holds its table and counts its guests, but is not
charged to the shift’s covers caps — the séminaire seated in a room those caps do not
describe. Use this, not a “block”, for a private event: a block closes a slot without
consuming covers, so sixty guests booked as a block would leave the kitchen’s budget
untouched and the restaurant would keep selling covers it cannot cook. It requires the
reservations:write:override scope; sending true without it is a 403 with
code: "insufficient_scope" and param: "excluded_from_shift_limits" — re-send without
the field and the booking is made normally. Sending false, or omitting it, needs no
scope. The flag stays until a PATCH changes it, it is on every read and webhook of this
booking, and it is recorded on the created event.
The flag lives on the hold, so converting an exempt hold carries the exclusion onto the
booking even though your body says nothing about it. That needs the same scope: without it
the conversion is a 403 with param: "excluded_from_shift_limits", the hold is left
standing and consumable, and no idempotency key is burned. Send
excluded_from_shift_limits: false to convert the hold as an ordinary booking instead —
giving the exclusion up needs no scope. On a conversion, and only there, omitting the field
and sending false are different requests: omitting it keeps whatever the hold said.
Because the booking is not charged to those caps, they do not refuse it either: sixty
exempt covers go into a shift with forty left, and no override is needed for the caps.
Nothing else is waived — the cutoff, the blocks, the party-size rules, the section and
the table all apply as they do to any booking, and a refusal from one of those still
needs its own override entry.
Booking a whole room. Send entire_section_id (a sec_… id from GET /v1/sections)
and the booking takes every table in that section — the séminaire that has the salon to
itself. You name the room; the restaurant’s floor plan decides which tables make it up,
read at the moment you book, on the floor plan in use for that service. tables on the
response lists all of them. It requires the reservations:write:override scope;
sending it without is a 403 with code: "insufficient_scope" and
param: "entire_section_id". It is not a stronger section_id: section_id asks for a
table somewhere in an area, and the two cannot be sent together.
The party size has no upper bound. A whole-room booking is exempt from the widget’s
max_party_size, no capacity is published anywhere for it to be measured against, and
the shift’s covers caps do not charge it — you know what you sold and what the room
holds, and we take the number. Every other booking keeps the ceiling it had:
max_party_size, overridable as usual.
One bound does remain, and it is the LOWER one: the widget’s min_party_size still
applies, so a room booked for a party below it is refused party_size_too_small like any
other booking, overridable in the same way.
A room that is partly taken is refused, never partly booked. If anything already holds
one of its tables during the window, the 422 carries section_occupied in violations,
not overridable, with a conflicts list: each occupied table and the time it is held
from and to — nothing about who holds it. That list is also the only way to learn whether
a room is free on a date; there is no availability search for whole rooms yet, so try the
booking and read the refusal. A room that is not drawn on the floor plan in use for that
service is refused with no_table_available_in_section.
A whole-room booking is excluded from the shift limits by default — send
excluded_from_shift_limits: false to charge it to the covers caps instead; either way
needs no scope beyond reservations:write:override. It lasts as long as the booking
rules say for its party size, or until the service closes with
duration: "until_shift_end", the only duration you can ask for. You cannot set an end
time. The room it took, and any duration you asked for, are recorded on the created
event as booked_entire_section and duration. A whole-room booking cannot be modified
— PATCH answers 409 section_booking_not_modifiable; cancel and rebook — and cannot be
held.
Converting a hold. Send hold_id and the slot comes from the hold — omit
service_date, start_time and party_size, or restate them exactly; a value that
disagrees is a 400, because a hold cannot be moved. The hold and the booking are the
same row and share their id’s suffix, so hold_AbC answers resv_AbC. The hold is
consumed atomically, so an unknown, already-converted, released or expired hold_id is a
404 — retry without it to book the slot fresh, which re-runs every capacity check.
Capacity, cutoff, blocks and the section are NOT re-checked on a conversion: the hold
already bought them, and re-checking would count the hold’s own covers against itself.
Everything about the guest still applies, including the card guarantee, so a conversion
onto a guaranteed slot lands awaiting_guarantee exactly as a fresh booking would.
metadata set on the hold is carried onto the booking unless you send new metadata.
⚠️ A conversion fires hold.converted AND reservation.created. They describe the same
party — treat the conversion as the hold being released, or you will count the covers
twice.
Writes have their own rate budget, lower than the read one. Creating a booking takes a
lock on the restaurant’s service day, so sustained writes queue every other writer —
including the restaurant’s own staff — behind them. Writes get 20 in a burst and 1 per
second sustained, with a daily ceiling of 2,000, per restaurant across all of its keys.
Reads are unaffected and keep their own, larger budget. When a response carries the
RateLimit-* headers, they describe the budget THAT request spent, and
RateLimit-Resource names it (read or write) — pace your writes off a write response,
not off a read one.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Unique per booking attempt; re-send the same value to retry safely. At most 255 characters, and no control characters: a longer or unprintable key is a 400 idempotency_key_invalid``, not a silently truncated one. Keys are scoped to your restaurant — another restaurant’s key of the same value is a different key — and are remembered for 72 hours, after which the same value starts a new request.
Pin this request to a dated version of the contract, e.g. 2026-09-01. Omit it and the version pinned on the API key applies. A version we do not publish is a 400 bad_request with param: "Service-Version". The version that was applied comes back on the Service-Version response header, always.
Query Parameters
Section titled “Query Parameters”Only modification_token is accepted here, and this is the only operation that accepts it. Adds the guest’s manage-link credential for the booking this request creates. Requires the guests:read scope; anything else is a 400.
Request Bodyrequired
Section titled “Request Bodyrequired”object
The service day the booking belongs to, YYYY-MM-DD in the restaurant’s timezone. A seating after midnight in a service that began the evening before keeps that evening’s date, so it can differ from the date in starts_at.
HH:MM on the restaurant’s clock: a slot time GET /v1/availability offered for this service_date.
How many guests the booking is for.
Seat in one area (sec_…). Resolves to the whole section group, exactly as the availability filter does.
Book the WHOLE section (sec_…): every table in it, as drawn on the floor plan in use for that service. Bounded by what the room holds rather than by max_party_size, excluded from the shift limits unless you say otherwise, and refused with section_occupied if anything already holds part of it. Requires the reservations:write:override scope. Not with section_id or hold_id.
Only with entire_section_id. until_shift_end holds the room until the service closes; omit it for the duration the booking rules give the party. There is no way to send an end time.
Convert a hold (hold_…) taken with POST /v1/holds. The slot comes from the hold; send no service_date, start_time, party_size or section_id, or restate them exactly. Unknown or already-consumed: 404.
An EXISTING guest (gst_…). The profile is never modified: contact sent alongside it is stored on this reservation only. Omit it and the guest is matched on email or phone, or created.
File the booking under one of the restaurant’s companies (cmp_…), as its own staff would — the id is the one company.id carries on a booking already filed under it. An id this restaurant does not know is a 404 naming company_id.
Required unless guest_id is given. First and last name are always required.
object
The guest’s first name. Required — unlike on a guest profile, where one of the two may be null. Names a new profile; an existing one is not renamed.
The guest’s last name. Required — unlike on a guest profile, where one of the two may be null. Names a new profile; an existing one is not renamed.
The guest’s e-mail address. Without guest_id, the guest is matched on it or on phone. It is also this booking’s contact_email, where our messages go. An existing profile is not changed by it.
The guest’s phone number, ideally E.164 (+33612345678); one without a country code is read in the restaurant’s country, and one that cannot be a phone number is a 400 with param: "guest.phone". Matched on and stored like email, as this booking’s contact_phone.
The language THIS GUEST is written to in — confirmation, reminder, modification
link, survey. Omit it and they are written to in the restaurant’s
primary_language, whoever they are; nothing in the response says which language
went out, so send it whenever you know it.
Stored on the profile and snapshotted onto this booking. An unsupported value is a
400 with param: "guest.language", never a silent fallback.
⚠️ Accepted platform-wide but HONOURED only within the restaurant’s
guest_languages (GET /v1/configuration): a language it does not publish is
recorded on the profile and the message still goes out in primary_language.
Unrelated to ?locale= on GET /v1/availability, which is the language WE answer
YOU in.
true when the guest agreed, in your flow, to receive the restaurant’s marketing e-mail. It records consent on the profile, even an existing one. false or omitted changes nothing: it never withdraws consent already given.
What the guest asked for, in their own words. The restaurant sees it on the booking. Reading it back follows the response’s guest_notes.
The restaurant’s own note about this booking, as its staff would type it into the reservation form. Never shown to the guest. Reading it back follows the response’s internal_notes, which needs guests:read — on this booking as on any other.
Your own reconciliation data, echoed verbatim on every read and webhook. Its keys are yours and never checked. Set here and not editable afterwards. With hold_id, a non-empty object replaces the hold’s metadata; omit it to keep the hold’s. At most 50 keys and 4,096 bytes.
object
Booking rules you accept breaking, for this booking only. Requires the
reservations:write:override scope. Each value names a rule, and sending it waives that
rule whether or not the refusal reported it — so you can send one ahead of the round trip.
guarantee_required is the rule “this slot asks the guest for a card”. You are only
REFUSED with it when the card cannot be asked for — no address the pay-link can reach —
but waiving it always means the same thing: the booking is made with no card guarantee at
all, and the guest is never asked for one.
Who writes to the guest about this booking. partner_managed means you do, and we send them nothing — including the modification link. Defaults to service_managed. Treat the list as open.
Do not charge this booking to the shift’s covers caps — a private event seated apart from the room those caps describe. It still holds its table.
Requires the reservations:write:override scope, and stays on the booking
until a PATCH changes it. Defaults to false, except on a whole-section booking, where
it defaults to true and needs no scope of its own.
With hold_id, the hold’s own value applies when this is omitted, and carrying an
exempt hold across needs the same scope; send false to convert it as an ordinary
booking.
Ask the guest for a card before the booking is held. The booking is created awaiting_guarantee and is confirmed when the card is saved.
We e-mail the guest a payment link. Send {} to ask on the restaurant’s own terms, or set
any of the three below. Needs no scope beyond reservations:write, and needs a guest
e-mail address — without one the request is a 422 email_required_for_guarantee.
Not the same thing as the slot’s own guarantee, which applies whether or not you send this
and is described by guarantee on GET /v1/availability. Sending this on such a slot
replaces the amount and deadline it would have used.
A restaurant that cannot take card payments refuses the request with 422 payments_suspended, unlike a booking on a guaranteed slot, which is simply confirmed
without a card.
object
How much to guarantee, in the restaurant’s currency’s smallest unit, for the whole party. Omit it for the amount the slot’s own guarantee policy would have used. Must be greater than zero; a slot with no policy and no amount here is a 422 guarantee_amount_invalid.
How long the guest has to save the card. Defaults to 48. Clamped, never refused: a deadline past the booked time — or past free cancellation, when that comes sooner — is brought forward, and the guarantee.expires_at on the response is the one that applies.
Cancel the booking when the deadline passes with no card saved. Defaults to true. Set it to false to keep the booking and let the restaurant decide.
Example
{ "service_date": "2026-10-16", "start_time": "19:30", "party_size": 2, "guest": { "first_name": "Camille", "last_name": "Dubois", }}Responses
Section titled “Responses”Reservation created
object
The object type: always reservation.
The booking’s id, resv_…. Opaque and permanent. A booking converted from a hold keeps that hold’s suffix: hold_AbC becomes resv_AbC.
Where the booking is in its lifecycle.
⚠️ held and awaiting_guest never appear on a reservation this API serves — a
hold is its own object (GET /v1/holds) and a waitlist promotion awaiting its guest
is not yet a booking — so no endpoint, and no reservation.* webhook, can carry
either value. They are listed because the enum is the platform’s own and codegen
reads it verbatim. awaiting_guarantee DOES appear: it is a booking waiting for the
guest to save a card.
There is no late. The restaurant’s screens show a party as late from its start
time, but that is worked out on the spot and never stored, so a late party keeps the
status it had, usually confirmed.
The mechanism that created the booking. See booking_channel for the channel the
demand arrived through — a waitlist booking promoted by staff still has the channel
the guest joined through.
manual: entered by the restaurant’s staff.widget: made by the guest in the restaurant’s booking widget, including a widget embedded on a partner’s page.walk_in: a party seated without a booking.import: brought over from the restaurant’s previous reservation system.waitlist: came out of the waitlist.api: created through this API (POST /v1/reservations).
The channel the demand arrived through, as opposed to source, the mechanism.
back_office: taken by the restaurant’s staff.website_widget: the restaurant’s own booking widget.phone: taken by telephone.walk_in: a party that walked in.google_reserve: booked through Google.partner_platform: taken in the booking widget embedded on a partner’s own page.api: created through this API (POST /v1/reservations), including a hold converted into a booking.
How many guests the booking is for.
The service day the booking belongs to, YYYY-MM-DD in the restaurant’s timezone. A seating after midnight in a service that began the evening before keeps that evening’s date, so it can differ from the date in starts_at.
When the party is due to sit down: ISO 8601 with the restaurant’s UTC offset, e.g. 2026-07-01T20:00:00+02:00.
When the table is expected back, in the same format as starts_at. null when no seating duration applies to the booking.
What the guest asked for, in their own words. Guest data: present only when your key
carries guests:read — on every booking alike, including the ones your key made.
Otherwise the field is absent, not null. Every webhook delivery carries it.
Who writes to the guest about this booking. partner_managed means we send none of the discretionary guest messages for it. Treat the list as open.
True when this booking does not count against the shift’s covers caps — a private event seated apart from the room those caps describe.
Those caps do not refuse it either, so a party larger than the covers left is booked rather than turned away. It still holds its table, and it still appears in every count of guests.
Setting it requires the reservations:write:override scope, except on a
whole-section booking (entire_section_id), where it is the default.
The e-mail address this booking’s messages go to, as given when it was made. null
means the booking uses the guest profile’s address. Guest data: present only when
your key carries guests:read — on every booking alike, including the ones your key
made. Otherwise the field is absent, not null. Every webhook delivery carries it.
The phone number this booking’s messages go to, as given when it was made. null
means the booking uses the guest profile’s number. Present on the same terms as
contact_email.
The restaurant’s own note about this booking — what its staff would type into the reservation form. Not shown to the guest anywhere.
Guest data, and gated exactly like guest_notes: present only when your key carries
guests:read — on every booking alike, including the ones your key made. Otherwise
the field is absent, not null. Every webhook delivery carries it.
The guest’s id, gst_… — the unexpanded form.
A guest profile from the restaurant’s guest book. Guest data: served by GET /v1/guests and ?expand=guest, both of which need the guests:read scope, and carried by every guest.* webhook delivery.
object
The object type: always guest.
The guest’s id, gst_…. Opaque and permanent. When two profiles are merged, the one that disappears keeps resolving to the survivor in ?id= on GET /v1/guests.
First name. A profile has a first name or a last name, possibly both; null when it has only the other.
Last name. null when the profile has only a first name.
Primary e-mail address, trimmed and lower-cased. null when none is known.
Primary phone number: E.164 (+33612345678) when it could be parsed for the restaurant’s country, otherwise as entered with separators removed — so do not assume E.164. null when none is known.
The restaurant’s own notes on the guest profile: free text written by its staff, or carried over from its previous reservation system — the restaurant’s words about the guest, not the guest’s. A booking’s guest_notes are the guest’s own. null when there are none.
Whether the restaurant has flagged this guest as unwelcome. A flag only: it does not by itself refuse a booking.
Why the guest was flagged — required by the restaurant when it sets blacklisted. null when the guest is not flagged.
Dietary preferences the restaurant recorded, as free-form labels. [] when none.
Allergies the restaurant recorded, as free-form labels. [] when none.
Date of birth, YYYY-MM-DD. null when not recorded.
An anniversary date the restaurant recorded, YYYY-MM-DD. null when not recorded.
Whether the restaurant marked the guest as a VIP.
The language the restaurant writes to this guest in, as an ISO 639-1 code. Set to the restaurant’s primary language when the profile is created unless one is given, so null only on a profile that never had one.
How the restaurant first learned of this guest. Not the same vocabulary as a
reservation’s source.
manual: entered by the restaurant’s staff.widget: first seen booking in the restaurant’s booking widget.import: brought over from the restaurant’s previous reservation system.host_assertion: first seen booking through the widget embedded on a partner’s page, with the partner vouching for their identity.api: first seen on a booking created through this API.website_newsletter: first seen signing up to the newsletter on the restaurant’s website.
Whether the guest agreed to receive marketing e-mail from the restaurant.
When marketing_email_consent last became true, ISO 8601 with the restaurant’s UTC offset. null while consent is false.
Whether the guest agreed to receive marketing text messages from the restaurant.
When marketing_sms_consent last became true, ISO 8601 with the restaurant’s UTC offset. null while consent is false.
How many of the guest’s bookings ended with them seated (seated, partially_seated or completed). Recomputed hourly, so it can trail a visit by up to about two hours.
How many of the guest’s bookings ended no_show. Recomputed like visit_count.
How many of the guest’s bookings were cancelled, counting only bookings whose time has passed — a cancelled booking for next month is not counted until then. Recomputed like visit_count.
When the guest’s earliest visit (a booking counted in visit_count) started: ISO 8601 with the restaurant’s UTC offset, on the calendar day it actually started — a seating after midnight carries the next day’s date. null until they have one. Recomputed like visit_count.
When the guest’s latest visit started, in the same format as first_visit_at. null until they have one. Recomputed like visit_count.
When the profile was created: ISO 8601 with the restaurant’s UTC offset.
When the profile last changed, in the same format. The value updated_since compares against.
The company the booking is made for, when it was filed under one — by the restaurant’s staff, or by company_id on your own create or PATCH. null otherwise, which is the common case; the key is always present. Readable by every key with reservations:read, unlike the guest’s own details.
object
The object type: always company.
The company’s id, cmp_….
The company’s name, as the restaurant recorded it.
The tables the booking is seated at, each with its seating area. [] while no table is assigned.
object
The object type: always table.
The table’s id, tbl_…. Opaque and stable.
The table’s name on the restaurant’s floor plan.
The smallest party the table is meant for. Always a number, never null.
The largest party the table seats on its own — a larger party needs tables joined together. Always a number, never null, and never below min_capacity.
The seating area the table is in.
object
The object type: always section.
Pass this as section_id to GET /v1/availability or to a create — any member of a group resolves to the whole group, so a sec_… read off a reservation’s table works without knowing which row it is.
The area’s name, in the restaurant’s primary_language.
The kind of space, as the restaurant classified it: indoor, outdoor, terrace,
bar, lounge, private_room (a room of its own), patio or rooftop. For a group,
take it from the row where group_id == id.
Whether THIS row is offered for online booking. Per row, never per group: it is deliberately not cascaded between group members, so a group is worth offering when ANY of its rows is true.
The sec_… id that identifies the conceptual seating area this row belongs to.
Rows sharing a group_id are ONE choice to a guest — the same area drawn on more than
one floor plan — so group by it before rendering a picker, or the same area is offered
twice.
Exactly one row per group has group_id == id: take that row’s name and
area_type as the group’s. The others are kept in step by the product and are
normally identical, but that is a behaviour of its editing paths rather than a
constraint, so the naming row is the answer that cannot go stale.
The card guarantee attached to the booking. null when it has none. Always included; no scope or expand needed.
object
The object type: always guarantee.
What the guest is guaranteeing with.
imprint: a saved card, charged only on a no-show or a late cancellation.prepayment: payment up front. Reserved: no guarantee is issued with it today.
Where the guarantee is in its own lifecycle, which is separate from the booking’s
status.
awaiting_card: waiting for the guest to save a card, untilexpires_at.active: a card is saved and can be charged under the terms the guest accepted.expired: the guest did not save a card in time.withdrawn: the restaurant withdrew the request before a card was saved.released: the card was let go and nothing will be charged.charged: the fullamountwas charged.partially_charged: less thanamountwas charged.charge_failed: a charge was attempted and declined; it may be retried.refunded: money charged was refunded in full.disputed: the guest disputed the charge with their bank.
How the card was asked for.
online_booking: the guest saved it while booking in the restaurant’s widget.staff_request: the guest was sent a link to save it — by the restaurant, or because the booking was created through this API.waitlist_offer: the guest saves it to claim a place offered from the waitlist.
The most that can be charged, as the guest accepted it: in the minor unit of currency (cents), so 4000 is €40.00. Covers the whole party.
ISO 4217 currency code of every amount on this object, e.g. EUR.
How much has been charged so far, in minor units of currency. 0 when nothing has.
How much of charged_amount has been refunded, in minor units of currency. 0 when nothing has.
null until the guest saves a card, and on a guarantee that never had one. Once a card is saved, its brand and the last four digits of its number; last4 can be null when the card network does not report it.
object
The card network, as the payment processor reports it, e.g. visa.
The last four digits of the card number.
Until when the guest can cancel without being charged: ISO 8601 with the restaurant’s UTC offset. A cancellation after it may be charged. null means cancelling is always free and only a no-show is charged.
While state is awaiting_card, the deadline for the guest to save a card, in the same format. null in every other state.
When the terms the guest accepts were recorded, in the same format: when the card was saved in the booking widget, or when the request was sent for a card asked for by link. null on a widget booking whose card is not saved yet.
When the guarantee was created, in the same format.
When the guarantee last changed, in the same format.
Your own reconciliation data, echoed verbatim on every read and webhook; {} when
there is none.
Set once, when the booking is made, and never changed afterwards — PATCH refuses
it. It comes from metadata on POST /v1/reservations, or from the hold the
booking was converted from. A booking made in the restaurant’s own widget carries
metadata only when the widget was embedded by a partner platform that signed it into
its identity assertion; an unsigned widget booking never has any.
⚠️ Readable by every integration this restaurant has authorised, not only by the one that wrote it. Put reconciliation ids here, not commercial terms.
object
The booking’s lifecycle events, oldest first — the same list GET /v1/reservations/{id}/events returns. Present only when you pass ?expand=events on a read, which needs no scope beyond reservations:read; absent otherwise, never an empty stand-in.
object
The object type: always reservation_event.
The event’s id, evt_…. Opaque and permanent. Not the id of the webhook event that announced the same change: the two are numbered separately.
What happened, named like the webhook event for the same change.
reservation.created: the booking was made.reservation.pending: it became a request awaiting the restaurant’s decision.reservation.confirmed: the restaurant accepted it.reservation.declined: the restaurant turned the request down.reservation.updated: its details or its tables changed.reservation.partially_seated: part of the party was seated.reservation.seated: the party was seated.reservation.completed: the party left.reservation.cancelled: it was cancelled —data.cancellation_reasonsays how.reservation.no_show: the party did not come.
When it happened: ISO 8601 with the restaurant’s UTC offset.
What changed, keyed per event type — statuses as from_status/to_status, field
edits under changes. Internal ids, staff identity and the staff’s own notes are
stripped. Treat the key set as open.
Six keys carry the guest’s own data and appear only when your key could read that
field on the booking — that is, when it carries guests:read:
contact_email,contact_phone,guest_notes— the booking’s own contact details and the note the guest left, insidechangeswhen one of them is edited.submitted_guest_name,submitted_email,submitted_phone— on thereservation.createdevent, what the booker actually typed, recorded when it differs from the guest profile the booking was matched to.
On reservation.cancelled, cancellation_reason says who ended the booking and how:
partner_apiis a cancel YOU made throughPOST /v1/reservations/{id}/cancel.guest_self_serviceis the guest cancelling from their own manage link.guarantee_expiredis a card request that lapsed unpaid.
Anything else was typed by the restaurant; match on the three named values and pass the rest through.
object
The guest’s own credential for their booking page. That page is on the booking host
that public_booking_url in GET /v1/configuration names, not on this API, and the
link is {public_booking_url}/{modification_token}, the same link the confirmation
e-mail sends the guest.
Whoever holds it can do on that page everything the guest can: read, change or
cancel the booking and, while it is awaiting_guarantee, complete the card step.
Pass it only to the guest, as the link to their own page; never log it, display it
or keep it longer than you need it. The endpoints behind that page are the booking
widget’s, not part of this API.
⚠️ Present ONLY on the POST /v1/reservations response, and only when you ask with
?expand=modification_token and your key carries guests:read (it reads the
guest’s contact details, so it is gated like the guest book). Reads, lists and
webhooks never carry it — capture it at create time. Never null when present.
How long it lasts. It is minted once, when the booking is created, and never
rotates: the value you capture is the value that booking keeps. It carries no
expiry of its own and stays usable for as long as the booking is live, which in
practice means until the party has been and gone. Cancelling — by you, by the
guest or by a card request lapsing — retires it, and so does the restaurant
marking the booking a no-show: the page answers 404 from then on, exactly as it
does for a token that was never issued. There is no way to
revoke it while the booking stands, which is the other half of “never log it”:
if it leaks, the remedy is to cancel and rebook.
When the booking was recorded: ISO 8601 with the restaurant’s UTC offset.
When anything on the booking last changed, in the same format. The value updated_since compares against.
Example
{ "object": "reservation", "status": "awaiting_guarantee", "source": "manual", "booking_channel": "back_office", "guest_notifications": "service_managed", "guest": { "object": "guest", "language": "fr", "source": "manual" }, "company": { "object": "company" }, "tables": [ { "object": "table", "section": { "object": "section", "area_type": "indoor" } } ], "guarantee": { "object": "guarantee", "kind": "imprint", "state": "awaiting_card", "origin": "online_booking" }, "events": [ { "object": "reservation_event", "type": "reservation.created" } ]}Headers
Section titled “Headers”Path to the created reservation
Dated API version applied
Present, as true, only when this response replays an earlier request with the same Idempotency-Key; the body is that original response, verbatim. Absent on a first write.
Malformed request, or no Idempotency-Key
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}No API key, or not a live one
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}Headers
Section titled “Headers”Unique id for this request. Quote it to support: it is how a specific call is found in our logs. Generated per request, or echoed from the X-Request-Id you sent.
The key does not carry reservations:write
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}No such hold, or it has already been used
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}This Idempotency-Key already stands for a different booking
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}The booking breaks one or more of the restaurant’s rules
The 422 a write answers with when it broke the restaurant’s booking rules: the envelope above plus the itemised violations. No other status carries that array.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Every booking rule this request broke, itemised. The whole array is the refusal: the top-level code is the fixed umbrella booking_rules_violated and the top-level message one fixed sentence, for one broken rule as for six. Only a 422 carries it.
object
The rule that was broken, e.g. cutoff_passed.
A human-readable explanation of this violation.
The request field to change to satisfy the rule, when there is one. Absent — not null — otherwise.
Whether a key carrying reservations:write:override may re-submit with this code in override and have the rule waived.
Only on section_occupied: what already holds part of the room during the window — one entry per occupied table, with the time it is held from and to. Never who holds it. Absent on every other code.
object
The occupied table (tbl_…). Null when the room has no tables and another whole-room booking holds it.
When the table is held from: HH:MM on the service_date the request named, in the restaurant’s timezone. Read on the service’s own axis, which runs past midnight — a 00:30 against a 23:30 is half an hour later the same night, not 23 hours earlier.
When the table is held until, on the same clock and the same axis as start_time. null for a booking stored without an end time.
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}Write rate limit exceeded
The envelope every public-API error answers with, whatever the status.
object
Every error response carries this one object.
object
The broad class of the error, from the HTTP status.
invalid_request_error: a400,403,404,409or422— something about the request to change.authentication_error: a401— the key is missing, invalid, revoked or expired.rate_limit_error: a429— slow down and retry.api_error: any other status, a fault on our side.
The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.
A human-readable explanation for the developer. Its wording can change.
The request parameter the error is about, when there is one — e.g. expand or limit. Absent — not null — when the error names no field.
A link to this code in the errors reference. Always present.
Example
{ "error": { "type": "invalid_request_error" }}Headers
Section titled “Headers”Writes permitted in the burst
Writes remaining in the burst
Seconds until the write bucket refills
Which budget the numbers above describe. write here.
Seconds to wait before retrying