Book past the rules
Some bookings are meant to break the rules. A corporate dinner of thirty on a shift that caps parties at eight. A regular the owner will seat whatever the covers count says. A group the restaurant sold by phone last month, into an evening that closed to online booking an hour ago.
Every one of those is a refusal on POST /v1/reservations, and every one of
them is a booking the restaurant wants. The override array is how you make it:
you name the rule you are setting aside, for that one write.
First: the waivable set is fixed, and short
Section titled “First: the waivable set is fixed, and short”Nine codes can be waived. The list is part of the API, not a restaurant setting — no restaurant can extend it, and no scope unlocks more of it.
| Waivable | |
|---|---|
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 |
Everything else is a wall. Two of the walls are the refusals a large-party desk meets most:
no_service_at_this_time— nothing serves that date and time. No service covers it, so there is no shift to seat the party in, no covers budget to waive and nothing an override could reach. The only remedy is a different slot: re-readGET /v1/availabilityand send a time it publishes. The restaurant can open a service for that evening, and then the booking works because the schedule changed, not because a rule was set aside.no_table_available— the covers were there and the geometry was not. Nothing on the floor plan seats that party at that time. Waiving it would produce a booking with no table, which the restaurant then has to render on a floor plan that has nowhere to put it. A corporate dinner that has a room to itself is booked withentire_section_idinstead, which takes every table in the section. See Book a whole room.
A retry loop that echoes back every code it was handed will never terminate on
those two. Read overridable on each violation and re-submit only the ones
that say true. The full list, with what each one means, is in
Booking-rule refusals.
Scopes for this job
Section titled “Scopes for this job”| Scope | Why this flow needs it |
|---|---|
reservations:write |
The create, the modify, the hold. |
reservations:write:override |
The override array itself. Separate from reservations:write so an ordinary integration cannot waive a rule by accident. It also carries excluded_from_shift_limits, for a private event that must not be charged to the service’s covers caps — see A corporate dinner needs one field — and entire_section_id. |
reservations:read |
Availability, to find the slot in the first place. |
Both write scopes are minted by the restaurant, on the key, in their own back
office. A key that does not carry one cannot grant it to itself, and a call that
asks for what the key lacks is refused with 403 insufficient_scope carrying
param: "override" — see Scopes.
That param is the difference between “this key cannot call this endpoint” and
“this key cannot call it that way”. The second is a retry worth automating:
drop the array, send the ordinary booking, and tell whoever is on the phone that
the rule stands.
Make the booking, and read the array
Section titled “Make the booking, and read the array”curl -X POST "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "service_date": "2026-10-04", "start_time": "20:00", "party_size": 30, "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]" }, "guest_notes": "Séminaire direction commerciale — salon sud" }'A booking that breaks rules comes back 422, with booking_rules_violated at
the top level and one entry in violations for each rule broken. The umbrella
code is there always, including when exactly one rule was broken, and it
never names a rule:
{ "error": { "type": "invalid_request_error", "code": "booking_rules_violated", "violations": [ { "code": "party_size_too_large", "message": "…", "param": "party_size", "overridable": true }, { "code": "cutoff_passed", "message": "…", "overridable": true } ] }}There is no version of this response that reports one rule flat. A client that
switches on error.code and finds party_size_too_large there is reading an
API that does not exist, and it will keep working right up to the evening a
booking breaks two rules at once.
Decide, then re-submit once
Section titled “Decide, then re-submit once”curl -X POST "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "service_date": "2026-10-04", "start_time": "20:00", "party_size": 30, "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]" }, "guest_notes": "Séminaire direction commerciale — salon sud", "override": ["party_size_too_large", "cutoff_passed"] }'Five things about that second call.
The array is itemised, and there is no blanket form. No "all", no
force: true, no wildcard. A boolean gets switched on once during integration
and then silently waives every rule written afterwards, including ones the
restaurant adds next year. An array names what you accept for this write, so
a rule that ships later is enforced for every existing integration, because
nobody has named it.
Same Idempotency-Key as the refused attempt. The refusal created nothing
and handed the key back, so re-presenting it claims it fresh rather than
replaying the refusal. The key stops a timed-out retry from booking the party
twice, so keep it for that window. See
Idempotency.
Retry once, not in a loop. Collect the overridable codes, decide, re-submit. A second refusal is a different problem with the booking, not a longer list to echo.
The second call can meet a wall the first refusal did not name. The table
search runs only once every rule has passed or been overridden, so
no_table_available is never listed next to party_size_too_large. A party of
thirty with the override accepted still needs a table, or a combination, that
seats thirty. Where the restaurant has none, the re-submission comes back 422
with no_table_available, and the booking cannot be made at that time. Show
that to whoever decided on the override, rather than asking them to decide
again. What one refusal cannot tell
you has the order the checks
run in.
You can send the array pre-emptively. A desk that knows it is booking a
thirty-cover corporate dinner can send override on the first call and skip the
discovery round trip. Nothing is recorded when nothing was waived, so an
override on a booking that broke no rule costs you nothing — see
What the restaurant sees.
Naming an unwaivable code is a 400, at request time
Section titled “Naming an unwaivable code is a 400, at request time”This is the part that closes the loop, and it is deliberate. Put
no_service_at_this_time in override and the whole request is refused with
400 bad_request and param: "override", before any booking work happens. The
message distinguishes the two mistakes:
`no_service_at_this_time` is a real violation but can never be overridden.`"slot_at_capacity"` is not a violation code.
Both then list the nine codes the array accepts.
An integrator meets that message while writing the integration, which is the
entire point. The alternative — dropping the unwaivable code and booking
anyway — hands back a 201 for a booking that waived nothing the caller thought
it had waived, and that is discovered on the floor at 20:00.
The same refusal covers the second mistake: the vocabulary in a planning
document or a support thread is not the emitted vocabulary. slot_at_capacity
and past_booking_cutoff are not codes this API sends, so a 400 naming them
is worth more than a silent no-op.
Where else the array works
Section titled “Where else the array works”override is accepted on three writes, and each is the right place for it:
| Write | Why |
|---|---|
POST /v1/reservations |
The booking itself. |
PATCH /v1/reservations/{id} |
Growing a party past the cap, or moving a booking past the cutoff, breaks the same rules a create does. |
POST /v1/holds |
The rules are checked when the hold is taken, and the conversion deliberately does not re-check capacity. So a thirty-cover hold on a shift with ten left has to carry its own override — there is no later call that could. |
A corporate dinner needs one field
Section titled “A corporate dinner needs one field”excluded_from_shift_limits: true says a booking must not be charged to the
service’s covers caps: the private event in the salon sud that does not draw on
the main room’s kitchen. It is the reason there is no “block thirty covers”
endpoint — a block closes a slot without consuming covers, so a private event
modelled as a block leaves max_covers untouched and the widget keeps selling
covers nobody can cook.
It is not a member of the override family, and the distinction survives every audit that looks at the booking afterwards. An override says “I accept breaking this rule, now, for this booking”. This says “this booking belongs to a different capacity pool”.
It is also more powerful than any override. An override is spent on one booking;
this is permanent, and it changes every availability read for that service from
then on. It had its own scope until 2026-09-22, and now shares
reservations:write:override — the claim stays distinct, the grant does not.
A key without the scope that sends true is refused 403 insufficient_scope
with param: "excluded_from_shift_limits". Sending false asks for nothing —
it describes the booking every key can already make — so it needs no scope, and
an integration that sends the field on every call is never refused for saying
no.
On a hold conversion the flag rides on the hold
Section titled “On a hold conversion the flag rides on the hold”This seam catches people. excluded_from_shift_limits is stored on the hold,
not restated in the conversion body, so a key converting an exempt hold is
asserting the exemption without a field in its request that says so.
Two keys make that concrete: one holds reservations:write:override and takes
the hold, a second, narrower key converts it. The conversion is refused —
403 insufficient_scope, param: "excluded_from_shift_limits" — rather than
converting the hold into an ordinary booking that quietly eats the evening’s
whole covers budget.
The refusal is reversible on purpose. The hold is left standing and still
consumable, and the message names the escape: send
excluded_from_shift_limits: false and the conversion goes through as an
ordinary booking, because that is the caller explicitly accepting the ordinary
classification.
What the restaurant sees
Section titled “What the restaurant sees”An override sets aside a rule the restaurant wrote. That is a real act, and it is recorded as one.
The reservation.created event on the booking carries overrides_accepted — the
codes that actually waived something on that write — and your key can read it
back:
curl "https://api.useservice.app/v1/reservations/resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ/events" \ -H "Authorization: Bearer $SERVICE_API_KEY"{ "object": "reservation_event", "type": "reservation.created", "data": { "source": "api", "party_size": 30, "overrides_accepted": ["party_size_too_large", "cutoff_passed"], "excluded_from_shift_limits": true }}Three things follow from how that datum is written, and they are worth knowing before you design around it:
- Applied, not requested. Sending the array pre-emptively on every call is a reasonable integration shape, and recording what was asked for would stamp an identical list on every booking and answer nothing. Only what genuinely waived a rule is recorded.
- Absent, not empty, when nothing was waived. An ordinary booking’s
createdevent is byte-identical whether it came from your integration, the widget or the back office. There is no “an override was considered” state. excluded_from_shift_limitsis recorded on the event as well as on the booking, because the column alone cannot say who set it. The back office writes the same column, so a booking that is exempt today may have been classified by your key or ticked by a manager, and the column reads the same either way. “Did this integration start exempting everything?” is a question an operator asks when a service oversells, and only a dated record at the moment of the assertion answers it.
The events feed is covered in Reservation
events. A flag-only change also emits a
reservation.updated webhook
carrying the previous value, so a restaurant watching its own integrations sees
the flip when it happens rather than when the service sells out.
The refusals this flow produces
Section titled “The refusals this flow produces”| Code | Where it arrives | What to do |
|---|---|---|
booking_rules_violated |
422 on the first attempt |
Read violations. Never the top-level code. |
bad_request, param: "override" |
400, before any booking work |
You named a code that can never be waived, or that is not a code. Fix the array. |
insufficient_scope, param: "override" |
403 |
The key cannot waive rules. Re-send without the array, or ask the restaurant for a key that can. |
insufficient_scope, param: "excluded_from_shift_limits" |
403 |
On a create, drop the field. On a conversion, send false. |
no_service_at_this_time |
inside violations, overridable: false |
A different slot. Nothing else works. |
no_table_available |
inside violations, overridable: false, often only on the re-submission |
A different time, a smaller party, or the restaurant rearranges the floor. For a party that takes a whole room, book the room. |
guarantee_required |
inside violations, overridable: true |
Waiving it books with no card held. See Take a deposit. |
Errors is the reference for all of them, and for the rest of the violation vocabulary.
What separates this from a force flag
Section titled “What separates this from a force flag”A demo hard-codes override and watches the booking go through. Four things
separate that from something a restaurant can live with.
It does not override by default. The array on every call, populated from
whatever the last refusal said, is a force: true written the long way. Decide
per booking, from the codes you actually received.
It asks a human, where a human would ask. party_size_too_large on a party
of thirty is a commercial decision the restaurant has already made — the dinner
is sold. slot_no_longer_available on a party of two is the covers
cap doing its job, and overriding it puts a table in a room the kitchen has
already committed. The codes are different so you can treat them differently.
It keeps its own record. overrides_accepted tells the restaurant what was
waived; it does not tell them who on your side decided, or why. Put the
reconciliation reference in metadata and the reason in guest_notes, which
the floor actually reads.
It leaves the override scope off the keys that do not need it. A public
booking funnel holds reservations:write and nothing more. A corporate-events
desk,
operated by people the restaurant knows, holds the override scope. One key per
job is the whole reason the scopes are separate, and a restaurant can revoke one
without breaking the other.