Skip to content

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-read GET /v1/availability and 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 with entire_section_id instead, 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.

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.

Terminal window
curl -X POST "https://api.useservice.app/v1/reservations" \
-H "Authorization: Bearer $SERVICE_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"service_date": "2026-10-04",
"start_time": "20:00",
"party_size": 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.

Terminal window
curl -X POST "https://api.useservice.app/v1/reservations" \
-H "Authorization: Bearer $SERVICE_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"service_date": "2026-10-04",
"start_time": "20:00",
"party_size": 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.

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.

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.

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:

Terminal window
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 created event 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_limits is 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.

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.

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.