Aller au contenu

Create a reservation

POST
/v1/reservations
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.

Idempotency-Key
required
string
<= 255 characters

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.

Service-Version
string

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.

expand
string

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.

Media typeapplication/json
object
service_date
required

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.

string format: date
start_time
required

HH:MM on the restaurant’s clock: a slot time GET /v1/availability offered for this service_date.

string
party_size
required

How many guests the booking is for.

integer
section_id

Seat in one area (sec_…). Resolves to the whole section group, exactly as the availability filter does.

string
nullable
entire_section_id

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.

string
nullable
duration

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.

string
nullable
Allowed values: until_shift_end
hold_id

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.

string
nullable
guest_id

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.

string
nullable
company_id

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.

string
nullable
guest

Required unless guest_id is given. First and last name are always required.

object
first_name

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.

string
last_name

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.

string
email

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.

string
nullable
phone

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.

string
nullable
language

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.

string
nullable
Allowed values: fr en de es it nl pt da sv no fi
marketing_email_consent

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.

boolean
nullable
guest_notes

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.

string
nullable
internal_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.

string
nullable
metadata

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
key
additional properties
any
override

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.

Array<string>
Allowed values: slot_no_longer_available cutoff_passed advance_window_exceeded party_size_too_small party_size_too_large slot_blocked shift_not_online_bookable duplicate_booking guarantee_required
guest_notifications

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.

string
Allowed values: service_managed partner_managed
excluded_from_shift_limits

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.

boolean
guarantee

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
amount_cents

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.

integer
nullable
deadline_hours

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.

integer
nullable
auto_cancel_on_expiry

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.

boolean
nullable
Example
{
"service_date": "2026-10-16",
"start_time": "19:30",
"party_size": 2,
"guest": {
"first_name": "Camille",
"last_name": "Dubois",
"email": "[email protected]"
}
}

Reservation created

Media typeapplication/json
object
object
required

The object type: always reservation.

string
Allowed values: reservation
id
required

The booking’s id, resv_…. Opaque and permanent. A booking converted from a hold keeps that hold’s suffix: hold_AbC becomes resv_AbC.

string
status
required

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.

string
Allowed values: awaiting_guarantee awaiting_guest held pending confirmed partially_seated seated completed cancelled no_show
source
required

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).
string
Allowed values: manual widget walk_in import waitlist api
booking_channel
required

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.
string
Allowed values: back_office website_widget phone walk_in google_reserve partner_platform api
party_size
required

How many guests the booking is for.

integer
service_date
required

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.

string format: date
starts_at
required

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.

string format: date-time
ends_at

When the table is expected back, in the same format as starts_at. null when no seating duration applies to the booking.

string format: date-time
nullable
guest_notes

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.

string
nullable
guest_notifications
required

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.

string
Allowed values: service_managed partner_managed
excluded_from_shift_limits
required

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.

boolean
contact_email

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.

string
nullable
contact_phone

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.

string
nullable
internal_notes

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.

string
nullable
guest
required
Any of:

The guest’s id, gst_… — the unexpanded form.

string
company
required

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
object
required

The object type: always company.

string
Allowed values: company
id
required

The company’s id, cmp_….

string
name
required

The company’s name, as the restaurant recorded it.

string
tables
required

The tables the booking is seated at, each with its seating area. [] while no table is assigned.

Array<object>
object
object
required

The object type: always table.

string
Allowed values: table
id
required

The table’s id, tbl_…. Opaque and stable.

string
name
required

The table’s name on the restaurant’s floor plan.

string
min_capacity
required

The smallest party the table is meant for. Always a number, never null.

integer
max_capacity
required

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.

integer
section
required

The seating area the table is in.

object
object
required

The object type: always section.

string
Allowed values: section
id
required

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.

string
name
required

The area’s name, in the restaurant’s primary_language.

string
area_type
required

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.

string
Allowed values: indoor outdoor terrace bar lounge private_room patio rooftop
bookable_online
required

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.

boolean
group_id
required

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.

string
guarantee

The card guarantee attached to the booking. null when it has none. Always included; no scope or expand needed.

object
object
required

The object type: always guarantee.

string
Allowed values: guarantee
kind
required

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.
string
Allowed values: imprint prepayment
state
required

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, until expires_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 full amount was charged.
  • partially_charged: less than amount was 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.
string
Allowed values: awaiting_card active expired released charged partially_charged charge_failed refunded disputed withdrawn
origin
required

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.
string
Allowed values: online_booking staff_request waitlist_offer
amount
required

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.

integer
currency
required

ISO 4217 currency code of every amount on this object, e.g. EUR.

string
charged_amount
required

How much has been charged so far, in minor units of currency. 0 when nothing has.

integer
refunded_amount
required

How much of charged_amount has been refunded, in minor units of currency. 0 when nothing has.

integer
card
required

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
brand
required

The card network, as the payment processor reports it, e.g. visa.

string
last4
required

The last four digits of the card number.

string
nullable
cancel_deadline_at
required

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.

string format: date-time
nullable
expires_at
required

While state is awaiting_card, the deadline for the guest to save a card, in the same format. null in every other state.

string format: date-time
nullable
consented_at

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.

string format: date-time
nullable
created_at
required

When the guarantee was created, in the same format.

string format: date-time
updated_at
required

When the guarantee last changed, in the same format.

string format: date-time
metadata

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
key
additional properties
any
events

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.

Array<object>
object
object
required

The object type: always reservation_event.

string
Allowed values: reservation_event
id
required

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.

string
type
required

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_reason says how.
  • reservation.no_show: the party did not come.
string
Allowed values: reservation.created reservation.confirmed reservation.declined reservation.updated reservation.pending reservation.partially_seated reservation.seated reservation.completed reservation.cancelled reservation.no_show
created_at
required

When it happened: ISO 8601 with the restaurant’s UTC offset.

string format: date-time
data
required

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, inside changes when one of them is edited.
  • submitted_guest_name, submitted_email, submitted_phone — on the reservation.created event, 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_api is a cancel YOU made through POST /v1/reservations/{id}/cancel.
  • guest_self_service is the guest cancelling from their own manage link.
  • guarantee_expired is 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
key
additional properties
any
modification_token

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.

string
created_at
required

When the booking was recorded: ISO 8601 with the restaurant’s UTC offset.

string format: date-time
updated_at
required

When anything on the booking last changed, in the same format. The value updated_since compares against.

string format: date-time
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"
}
]
}
Location
string

Path to the created reservation

Service-Version
string

Dated API version applied

Idempotent-Replayed
string
Allowed values: true

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

Media typeapplication/json

The envelope every public-API error answers with, whatever the status.

object
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
Example
{
"error": {
"type": "invalid_request_error"
}
}

No API key, or not a live one

Media typeapplication/json

The envelope every public-API error answers with, whatever the status.

object
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
Example
{
"error": {
"type": "invalid_request_error"
}
}
X-Request-Id
string

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

Media typeapplication/json

The envelope every public-API error answers with, whatever the status.

object
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
Example
{
"error": {
"type": "invalid_request_error"
}
}

No such hold, or it has already been used

Media typeapplication/json

The envelope every public-API error answers with, whatever the status.

object
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
Example
{
"error": {
"type": "invalid_request_error"
}
}

This Idempotency-Key already stands for a different booking

Media typeapplication/json

The envelope every public-API error answers with, whatever the status.

object
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
Example
{
"error": {
"type": "invalid_request_error"
}
}

The booking breaks one or more of the restaurant’s rules

Media typeapplication/json
Any of:

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
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
violations
required

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.

Array<object>
object
code
required

The rule that was broken, e.g. cutoff_passed.

string
message
required

A human-readable explanation of this violation.

string
param

The request field to change to satisfy the rule, when there is one. Absent — not null — otherwise.

string
overridable
required

Whether a key carrying reservations:write:override may re-submit with this code in override and have the rule waived.

boolean
conflicts

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.

Array<object>
object
table
required

The occupied table (tbl_…). Null when the room has no tables and another whole-room booking holds it.

string
nullable
start_time
required

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.

string
end_time
required

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.

string
nullable
Example
{
"error": {
"type": "invalid_request_error"
}
}

Write rate limit exceeded

Media typeapplication/json

The envelope every public-API error answers with, whatever the status.

object
error
required

Every error response carries this one object.

object
type
required

The broad class of the error, from the HTTP status.

  • invalid_request_error: a 400, 403, 404, 409 or 422 — something about the request to change.
  • authentication_error: a 401 — the key is missing, invalid, revoked or expired.
  • rate_limit_error: a 429 — slow down and retry.
  • api_error: any other status, a fault on our side.
string
Allowed values: invalid_request_error authentication_error rate_limit_error api_error
code
required

The specific error, stable and machine-readable, e.g. not_found. Branch on this, not on message.

string
message
required

A human-readable explanation for the developer. Its wording can change.

string
param

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.

string
doc_url
required

A link to this code in the errors reference. Always present.

string
Example
{
"error": {
"type": "invalid_request_error"
}
}
RateLimit-Limit
integer

Writes permitted in the burst

RateLimit-Remaining
integer

Writes remaining in the burst

RateLimit-Reset
integer

Seconds until the write bucket refills

RateLimit-Resource
string

Which budget the numbers above describe. write here.

Retry-After
integer

Seconds to wait before retrying