Take a deposit
Some bookings are worth money before anyone sits down. A corporate dinner, a party of thirty on New Year’s Eve, a table held over a bank holiday — the bookings a restaurant cannot afford to have evaporate. For those, a restaurant configures a guarantee: the guest saves a card, and the restaurant may charge it if nobody turns up.
You can book those slots. What you cannot do is take the card.
Does this venue ask for a card?
Section titled “Does this venue ask for a card?”Ask once per venue, before you build anything. GET /v1/configuration carries
card_guarantee_from_party_size:
null: no booking made now, on any service or date, is asked for a card. Either the restaurant uses no card guarantee, or it cannot take one at the moment. Skip the card step for this venue.- A number: some service, on some date, may ask a party of that size or larger, and no service asks a smaller party. Build the card step, then read the exact answer for the date the guest picks.
The exact answer is guarantee_from_party_size on each service in
GET /v1/availability: the smallest party that service asks for a card on that
date, whatever party_size you queried with. null there means no party is
asked on that service that day. guarantee on the same service carries the
amounts, as Read the quote sets
out.
The venue value can change at any time with no change on your side: when the
restaurant turns guarantees on or off, or when its card payments stop or
resume. If you keep it for longer than a guest’s session, revalidate it with the
ETag.
A restaurant whose card payments are not active asks for no card, and the create
is not refused for it. The booking is taken without a guarantee: confirmed, or
pending under manual approval. Both fields read null in that state, so they
already describe what the create will do.
The card step ends on a page you do not own
Section titled “The card step ends on a page you do not own”This is the constraint, and it decides the shape of the work before anything else does.
A booking on a guaranteed slot needs a payment credential. Your key reads the quote — mode, amount, cancellation terms — and never the credential behind it: no response to your key carries a client secret, a connected-account id or a publishable key. That is a v1 position rather than an oversight.
The guest saves their card on a Service-hosted page, by following a link we email them.
So a flow that expects to finish inside your own checkout cannot be built here today. What you get instead is this:
- Your create returns
201, with the booking inawaiting_guarantee. - Service emails the guest a pay-link.
- The guest saves a card on our page.
- The booking becomes
confirmed, orpendingwhen the service asks the restaurant to approve its bookings.reservation.createdfires then, with that status.
Your confirmation screen tells the guest to expect that email. If your product promises one uninterrupted checkout, decide about guaranteed slots on day one — either route them somewhere else, or set expectations in the copy. Finding this out after the funnel is built is expensive.
If you need the card taken inside a booking flow you control, embed the Booking Widget instead. It takes the card in-page, because it is our page.
The guest token is the guest’s own key
Section titled “The guest token is the guest’s own key”?expand=modification_token on the create returns the guest’s own credential
for their booking page. That page is not on api.useservice.app. It is on the
booking host, the one public_booking_url in GET /v1/configuration names
(https://book.useservice.app/r/{slug} in production), and the link is
{public_booking_url}/{modification_token}: the same link the confirmation and
pay-link emails carry.
Whoever holds the token can do on that page everything the guest can: read the
booking, change it, cancel it, and, while the booking is awaiting_guarantee,
save the card. The page’s own data carries what its card form is mounted with.
So the promise above covers your key and not the token. Once you ask for the
token, you hold the guest’s key to their booking.
Treat it like the guest’s password:
- Pass it only to the guest, as the link to their own page.
- Never log it, and never render it as text.
- Keep it no longer than you need it. If you hand the guest the link once, do not store it afterwards.
The endpoints behind that page belong to the widget and are not part of this
API. Build on the link, not on them. If you send the guest no link of your own,
skip the expand. Whether Service sends the link follows guest_notifications,
and on this flow it always does: partner_managed is refused on a slot that
takes a card, so the confirmation and pay-link emails carry it.
Scopes for this job
Section titled “Scopes for this job”| Scope | Why this flow needs it |
|---|---|
reservations:read |
Availability — the only place the guarantee quote appears before you book. |
reservations:write |
The create. |
reservations:write:override |
Only to waive the guarantee with override: ["guarantee_required"] on the create — booking the table with no card held. A PATCH cannot waive it. |
guests:read |
Only for ?expand=modification_token on POST /v1/reservations, the guest’s own key to their booking page. See The guest token is the guest’s own key. Reading back the contact details of the booking you just took needs it too. |
There is no guarantee scope, because there is no guarantee endpoint. Charging,
refunding and releasing a card are the restaurant’s acts, done from their back
office, and your key reads the outcome without causing it. Asking for a card in
the first place is a field on the create and the PATCH, and it needs nothing
beyond reservations:write — see Ask for a card
yourself. See Scopes.
Read the quote before you promise anything
Section titled “Read the quote before you promise anything”GET /v1/availability carries a guarantee object per shift. null means no
card will be asked for. Anything else describes what a booking in that shift
must guarantee, priced for the party size you asked about.
{ "object": "availability_guarantee", "mode": "imprint", "currency": "EUR", "per_guest_amount": 2000, "amount": 60000, "cancel_hours": 48}| Field | What it means |
|---|---|
mode |
imprint today, and only imprint. See Imprint, not charge. |
per_guest_amount |
Minor units per guest — 2000 is €20.00. |
amount |
Minor units for the party size you asked about. The figure to show. |
cancel_hours |
Hours before the seating after which a cancellation may be charged. null means cancelling is always free and only a no-show is charged. |
It is party-size-aware and it is emitted only when an imprint would genuinely trigger, so honouring it is how a funnel avoids telling a guest a booking is free and then producing one that waits on a card. Put the figure and the cancellation terms on the page where the guest picks their time.
Imprint, not charge
Section titled “Imprint, not charge”An imprint saves the card and authorises an amount. No money moves at booking
time. The restaurant may charge some or all of it afterwards — a no-show, or a
cancellation inside the cancel_hours window — and the guarantee on the booking
reports both figures separately:
amount is the consented ceiling, charged_amount what has been taken and
refunded_amount what has been given back. Every field, and the states a
guarantee moves through, are on The guarantee
object.
One value is yours to expect: origin is staff_request on every booking your
key creates. The card is asked for by a pay-link, the same request a restaurant
sends from its back office.
kind publishes two values, imprint and prepayment. Only imprint occurs
today; charge-up-front is a later phase and no slot produces it. Read the field
rather than assuming, and treat an unfamiliar value the way you treat any other
forward-compatible enum.
No Stripe identifier appears anywhere in this object, and no dispute status. Those are the restaurant’s relationship with their payment provider, and there is nothing you could do with them.
The five
reservation.guarantee_* webhooks — requested,
charged, refunded, released, payment failed — are how you learn that any of this
moved without polling for it.
Take the booking
Section titled “Take the booking”curl -X POST "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "service_date": "2026-12-31", "start_time": "20:00", "party_size": 30, "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]", "language": "fr" } }'Nothing in that body mentions the guarantee. The slot decides, and the response
tells you which fork it took: read status. awaiting_guarantee means the
table is held and the card is not saved yet.
Four things follow from that booking existing in that state.
The guest has 48 hours, or less. The pay-link request carries a deadline of
48 hours, clamped to the booked time and to the free-cancellation deadline when
either is sooner. The guarantee’s expires_at is the real figure — read it
rather than computing your own.
The table is held for the whole window. This kind of request does not hand the table back when the deadline passes; it holds it until the request is resolved. So leaving unpaid requests standing costs the restaurant real capacity.
Unpaid means cancelled, automatically. If the guest never saves a card, the request lapses and the booking is cancelled. Auto-cancel is on, because a restaurant that configured a guarantee did so precisely to avoid seating a party that never guaranteed.
Your 201 arrives before the reservation.created webhook does — and if
the guest never pays, that webhook never fires at all, nor does a
reservation.cancelled. A booking your system was never told about does not get
a cancellation. Reconcile from the API, as Keep your system in
sync
sets out.
Ask for a card yourself
Section titled “Ask for a card yourself”The slot decides on its own, and so can you. Send a guarantee object on a
create or on a PATCH, and the guest is asked for a card on a booking that
would otherwise take none. It is the same request the restaurant’s own staff
raise from their back office, on the same terms, and everything above about the
pay-link, the deadline and the hold on the table applies to it unchanged.
curl -X POST "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "service_date": "2026-12-31", "start_time": "20:00", "party_size": 6, "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]" }, "guarantee": { "amount_cents": 6000, "deadline_hours": 24, "auto_cancel_on_expiry": true } }'All three keys are optional, and {} asks for a card on the restaurant’s own
terms.
| Key | What omitting it means |
|---|---|
amount_cents |
The amount the restaurant’s own guarantee policy resolves for this service and this party size. |
deadline_hours |
48 hours. A deadline you send is clamped the same way — to the booked time, and to the free-cancellation deadline when that is still ahead. |
auto_cancel_on_expiry |
true: an unpaid request cancels the booking when the deadline passes. |
amount_cents and deadline_hours are whole numbers greater than zero.
Anything else is a 400 naming guarantee.amount_cents or
guarantee.deadline_hours, and auto_cancel_on_expiry takes true or false.
reservations:write is the whole of it. Asking for a card takes nothing
from the restaurant, adds a protection it would otherwise not have, and cannot
seat a party its rules would turn away — so it sits outside
reservations:write:override, which is the scope for booking past those rules.
Staff can withdraw the request from the back office at any point.
You cannot own the guest’s messages and ask for a card in the same breath.
guarantee sent beside guest_notifications: "partner_managed" is a 400
naming guarantee. The pay-link is an email from Service, and
partner_managed is you telling Service not to write to this guest. Send
guest_notifications as service_managed, or drop guarantee.
On a PATCH the card is priced after the edits apply. One body that grows
the party to six and asks for a card prices the card for six. The booking has to
be an upcoming, confirmed one that carries no guarantee yet;
guarantee_request_not_allowed
refuses every other state.
A card slot and an explicit request part company when card payments are off.
On a restaurant that cannot take charges, a slot that would ask for a card takes
the booking and confirms it with no guarantee at all — you are told nothing,
because nothing was refused. An explicit guarantee on that same restaurant is
refused with payments_suspended. You asked
a question, so you get an answer.
Four refusals belong to asking:
payments_suspended,
guarantee_request_not_allowed,
guarantee_guest_email_required
and guarantee_amount_invalid. A
create with no email address to send the pay-link to is refused earlier, with
email_required_for_guarantee,
whose message names which of the two asked for the card.
Changing the booking before the card is saved
Section titled “Changing the booking before the card is saved”A PATCH on a booking still in awaiting_guarantee applies like any other: the
new slot is re-checked against the rules, the table is picked again, and the
booking stays awaiting_guarantee until the card is saved.
The card request does not follow the change. amount, cancel_deadline_at
and expires_at stay exactly as your create computed them, whatever you move. A
party grown from four to six on a per-guest guarantee is still asked for the
four-guest amount, and the guest’s page still shows that amount. If the change
should be priced afresh, cancel the booking and take a new one.
A change that would newly put a card-less booking onto a guarantee is
refused, with 422
guarantee_modify_requires_card.
A modification cannot collect a card, on your door or on the guest’s: their own
booking page answers the same refusal, and tells them to cancel and book again.
Nothing is changed either way. See You cannot raise a consented
amount for the neighbouring refusal on a
booking that already has a card.
Until the card is saved — or the request lapses and the booking is cancelled —
no reservation.* event about the booking reaches you, changes included. A
PATCH in that window sends no reservation.updated, and neither
?expand=events nor the events feed lists it. The guest is not written to about
it either: a change made before the booking is
published sends them no message.
The reservation.created you receive, and the confirmation the guest receives,
describe the booking as it stands when it is published, not as it was first
requested.
Two refusals that surprise people
Section titled “Two refusals that surprise people”An email is mandatory here, whatever the flags say
Section titled “An email is mandatory here, whatever the flags say”A create on a guaranteed slot with no email address is refused with
email_required_for_guarantee.
This is the single place on this API where a guest email is not optional.
The surprise is that the configuration’s advisory flags do not reach it.
require_guest_email and require_guest_phone describe what the restaurant
asks for on its own booking form and bind nothing here — a create with neither
is accepted on an ordinary slot. On a guaranteed slot the requirement is
mechanical rather than a policy: the pay-link is an email, and without an
address there is nowhere to send it and no way for the booking to complete.
It arrives inside violations alongside
guarantee_required, and the order is
deliberate. email_required_for_guarantee comes first and is not
overridable; guarantee_required follows and is. Read top-down and the
first remedy you find is the one that costs no new privilege:
- Send an email address, or reuse a
guest_idwhose profile carries one. - Or
override: ["guarantee_required"], which books the table with no guarantee at all. That is the phone-booking case — you do not ask someone on the phone for a card imprint — and the restaurant then holds nothing against a no-show. Waive it deliberately, and see Book past the rules for what gets recorded when you do.
A related refusal sits next to it:
guest_notifications_required_for_guarantee,
returned when you assert guest_notifications: "partner_managed" — you own
guest communication for this
booking — on a
guaranteed slot. The guarantee is an email: pay-link, reminder, receipt. So
the assertion and the slot cannot both be honoured. Drop the suppression for
that booking, or waive the guarantee and leave no pay-link to suppress.
You cannot raise a consented amount
Section titled “You cannot raise a consented amount”Grow a party from four to eight on a booking whose card was saved through the
restaurant’s own widget, and the PATCH is refused with
guarantee_consent_guest_only.
Nothing changes — the whole modify rolls back, including every other field in
the same body.
The reason is not a permission model. A higher amount is a higher ceiling the restaurant may one day charge, and raising it needs the cardholder’s agreement to the new figure. That agreement is archived evidence from the person who gave it. Your key is not the guest, and neither is the restaurant’s own staff: nobody can assert it on their behalf.
Three ways forward, and the refusal names them:
- Make a change that does not raise the amount. Moving the day or the time applies, and the free-cancellation deadline follows the new seating. So does shrinking the party — the imprint is re-priced downward with nobody asked, because the existing, higher mandate already covers the smaller figure.
- Ask the restaurant. Back-office staff can change the party size, and the guarantee keeps its existing, lower ceiling.
- Hand the guest their own booking page. It is the only surface that can
collect a renewed mandate. Build the link from
public_booking_urlon the configuration and themodification_tokenyou asked for on the create — the same pair the booking funnel uses.
This refusal cannot arise on a booking your key created. A booking taken through
the restaurant’s own widget carries a card the guest entered and agreed an
amount for, and a higher amount is a new agreement only the guest can give. A
booking your key created carries a card requested through the pay-link
(origin: "staff_request"), at the amount the restaurant’s rules set or the
amount you asked for. No figure was ever put in front of that guest to agree to,
and the card is never re-priced afterwards, so there is nothing for this code to
refuse. You meet it anyway, because a sync hands you the restaurant’s widget
bookings and your modify path then runs over them.
A pay-link card keeps the amount it was requested at. A create for four on a guarantee of €10 a guest asks for €40, and the guest saves the card at €40. Grow the party to six a week later and the change goes through, with the card still at €40. The cover is no longer proportional to the party, so a late cancellation or a no-show on that booking can be charged up to €40 and no more. A restaurant that minds the gap can release the card from its back office and request a new one at the new figure. A widget booking behaves the other way round: it re-prices, and asks the guest to agree to the higher amount.
A booking with no card behind it meets a different refusal when a PATCH
would bring a guarantee on — a party grown past the size at which the restaurant
asks for a card, or a move to a service that asks every booking for one. That is
guarantee_modify_requires_card,
and nothing is written. No modification has a card step — not yours, and not the
guest’s own booking page, which answers the same refusal and tells them to
cancel and book again. Only a create takes a card: cancel and rebook, and the
new booking goes through the card step described above.
The refusals this flow produces
Section titled “The refusals this flow produces”| Code | When | What to do |
|---|---|---|
email_required_for_guarantee |
422, inside violations, overridable: false |
Send an email address, or waive the guarantee. |
guarantee_required |
422, inside violations, overridable: true |
Waiving books the table with no card held. |
guest_notifications_required_for_guarantee |
422, param: "guest_notifications", overridable: false |
Drop the suppression, or waive the guarantee. |
guarantee_consent_guest_only |
422 on a PATCH |
One of the three remedies above. |
guarantee_modify_requires_card |
422 on a PATCH, never overridable |
Cancel and rebook — only a create takes a card. The guest’s own page answers the same refusal. |
guarantee_modify_locked |
403 |
The free-cancel deadline has passed on an active imprint. The booking can still be cancelled, under the restaurant’s terms. |
guarantee_guest_email_required |
422 |
The guarantee step was reached with no contact email. Send a guest with one. |
guarantee_amount_invalid |
422 |
The restaurant’s configuration resolves to a non-positive amount. Send guarantee.amount_cents yourself, or report it to them. |
payments_suspended |
422 on a guarantee you sent |
The restaurant cannot take card payments. Drop guarantee and take the booking unguaranteed. |
guarantee_request_not_allowed |
422 on a guarantee you sent |
The booking is not an upcoming, confirmed one with no guarantee yet. Re-read it before asking again. |
Card guarantees is the reference for all of them.
Before you put this in front of a restaurant
Section titled “Before you put this in front of a restaurant”A demo books a guaranteed slot and screenshots the 201. Five things separate
that from something a restaurant will let near New Year’s Eve.
It discloses the amount before the guest commits. The quote is on availability precisely so the guest sees the figure and the cancellation terms on the page where they choose the time, not in an email afterwards.
It renders three outcomes, not one. confirmed, pending and
awaiting_guarantee are three different things to tell a guest, and only the
first means the table is theirs. A booking that leaves awaiting_guarantee can
land on either of the other two. A confirmation screen with one message is wrong
two-thirds of the time on a restaurant that uses guarantees.
It tells the guest about the email. The pay-link is the next thing that has to happen and it happens somewhere else. A guest who does not know to expect it does not open it, and the booking cancels itself two days later.
It sends guest.language. The pay-link, the reminder and the receipt all go
out in the guest’s language if you set it, and in the restaurant’s primary
language if you do not. A payment request somebody cannot read is a payment
request they do not action.
It never treats awaiting_guarantee as a booking. Not in its own reporting,
not in a covers count, and not in anything it sends the guest. It is a table
held against a promise that has not been kept yet.