Aller au contenu

Hold a slot

POST
/v1/holds
curl --request POST \
--url https://api.useservice.app/v1/holds \
--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 }'

Claims a slot without booking it, so you can assemble the rest of a package, take a payment, or wait for a prepayment to clear while the table stays yours. The table is occupied from the moment this returns — a hold counts against availability exactly as a booking does. Requires the reservations:write scope; there is no separate hold scope.

A hold is not a reservation. It has no guest and it does not appear in GET /v1/reservations. Convert it with POST /v1/reservations and a hold_id, give it back with DELETE /v1/holds/{id}, or let it expire.

Read expires_at from the response; never assume your own. You may request a deadline and the server decides, clamping rather than refusing. A basket hold lasts between 5 and 30 minutes, and 10 when you ask for nothing — the same everywhere, at every restaurant. Ask for what your checkout needs, up to thirty; expires_at says what any one hold actually got. A commitment hold is granted at most 48 hours.

basket and commitment differ at the deadline — by minutes, not by outcome. Both kinds lapse at expires_at and neither needs anybody to resolve it. What differs is when the table goes back on sale. basket (the default) is for assembling a package: the table reopens the instant expires_at passes, and status reads expired from that same instant, so an abandoned basket costs the restaurant nothing. commitment is for capacity you have already sold and are waiting to be paid for: it keeps its table past the deadline until a sweep picks it up — the sweep runs every five minutes — and until that runs the hold still reads held and still occupies. Releasing it at read time would put a table somebody has already bought back on sale for anyone. Ask for commitment only when that is what you mean, and treat its deadline as “the table goes within about five minutes”, not as a moment you can hold a reader to.

A hold and the reservation it becomes are the same booking: hold_AbC converts into resv_AbC. Once it converts, the hold leaves the GET /v1/holds list (the booking is in GET /v1/reservations), but GET /v1/holds/{id} still resolves it, with status: "converted" and the booking’s id in reservation.

The rules are checked here, not at conversion. Capacity, cutoff, blocks and the section are all evaluated when the hold is taken — that is what the hold buys — and the conversion does not re-check them. So override belongs on THIS call: sixty ordinary covers into a service with forty left is a 422 booking_rules_violated here, carrying slot_no_longer_available in violations, and override: ["slot_no_longer_available"] is how you place it deliberately. A hold taken with excluded_from_shift_limits: true is not charged to those caps at all, so they do not refuse it and no override is needed for them — the cutoff, the blocks, the section and the table still apply. Refusals on this endpoint take the same shape as on POST /v1/reservations: always that top-level code and a violations array, one broken rule or six — read the array, never the code.

Idempotency-Key is REQUIRED, for the same reason it is on the create and one of its own: a duplicate hold silently takes a second table, and because the id you lost is the only way to name it, you cannot DELETE what you did not receive. Retrying with the same key replays the original response. The requested expires_at is deliberately NOT part of what the key identifies, so a client that recomputes “now + 10 minutes” on every attempt still gets its replay.

A field this operation does not accept is a 400 naming it in param, never a silent no-op. 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.

Holds draw on the WRITE rate budget: 20 in a burst, 1 per second sustained, 2,000 a day, per restaurant across all of its keys.

Idempotency-Key
required
string
<= 255 characters

Unique per hold 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.

Media typeapplication/json
object
service_date
required

The service day of the held seating, YYYY-MM-DD in the restaurant’s timezone. A seating after midnight keeps the date its service began on.

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 held seating is for.

integer
kind

Defaults to basket. commitment holds the table past its deadline until it is resolved — ask for it only for capacity you have sold.

string
Allowed values: basket commitment
expires_at

The deadline you would like. Clamped, never refused; the response carries the one that was granted.

string format: date-time
nullable
section_id

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

string
nullable
metadata

Your own reconciliation data. Echoed here and carried onto the reservation when the hold converts.

object
key
additional properties
any
override

Booking rules you accept breaking, for this hold 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
excluded_from_shift_limits

Do not charge the resulting booking to the shift’s covers caps. Requires the reservations:write:override scope.

boolean
Example
{
"service_date": "2026-10-16",
"start_time": "19:30",
"party_size": 2
}

Slot held

Media typeapplication/json

A claim on a slot that occupies capacity without being a booking. It holds a real table, so it counts against availability for as long as it lives.

object
object
required

The object type: always hold.

string
Allowed values: hold
id
required

Opaque hold id (hold_…). A hold and the reservation it becomes are the same row and share this id’s suffix, so hold_AbC converts into resv_AbC.

string
kind
required

basket releases the table the instant expires_at passes; commitment keeps it until the sweep that runs every five minutes picks the hold up. Both lapse on their own; only the minutes between the deadline and the table reopening differ.

string
Allowed values: basket commitment
status
required

Lifecycle of the hold itself, not of a reservation.

  • held: live and occupying a table. The only state a conversion works from.
  • expired: the deadline passed and the table is back on sale. On a basket hold this is reported from the moment expires_at passes, because that kind reopens its table at read time. On a commitment hold the table is held past the deadline until the five-minute sweep resolves it, so the hold reads held for up to five minutes after expires_at and expired from then on. Either way it is the same lapse, and hold.expired announces it.
  • released: handed back deliberately — you called DELETE /v1/holds/{id}, or the restaurant released it from its own board.
  • converted: it became the booking named in reservation.
string
Allowed values: held expired released converted
party_size
required

How many guests the held seating is for.

integer
service_date
required

The service day of the held seating, YYYY-MM-DD in the restaurant’s timezone. A seating after midnight keeps the date its service began on.

string format: date
starts_at
required

Start of the SEATING the hold covers — not the hold deadline. ISO 8601 with the restaurant’s UTC offset.

string format: date-time
ends_at

End of the seating the hold covers, in the same format. null when no seating duration applies.

string format: date-time
nullable
expires_at
required

When the hold lapses. Authoritative: the server sets it, clamping whatever was requested. Null once the hold is over.

string format: date-time
nullable
ended_at
required

When the hold was released or lapsed. Null while it is live, and null once it has converted — a converted hold did not end, it became the booking named in reservation.

string format: date-time
nullable
reservation
required

The booking this hold became (resv_…), or null.

string
nullable
tables
required

The tables the hold occupies, each with its seating area.

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

True when this hold does not count against the shift’s covers caps, with the same meaning as on a reservation: they neither charge it nor refuse it. It is carried onto the booking when the hold converts, so converting an exempt hold needs the reservations:write:override scope.

boolean
metadata
required

Your own reconciliation data, echoed verbatim. Carried onto the reservation when the hold converts. ⚠️ Readable by every integration this restaurant has authorised, not only by the one that wrote it: holds are scoped to the restaurant, so another of its API keys can read this hold and this object. Put reconciliation ids here, not commercial terms or anything you would not show a competitor.

object
key
additional properties
any
created_at
required

When the hold was taken: ISO 8601 with the restaurant’s UTC offset.

string format: date-time
updated_at
required

When the hold last changed, in the same format.

string format: date-time
Example
{
"object": "hold",
"kind": "basket",
"status": "held",
"tables": [
{
"object": "table",
"section": {
"object": "section",
"area_type": "indoor"
}
}
]
}
Location
string

Path to the created hold

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"
}
}

This Idempotency-Key already stands for a different hold

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 hold 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"
}
}

Too many requests on the write budget

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

Requests permitted in the write burst

RateLimit-Remaining
integer

Requests remaining in the write burst

RateLimit-Reset
integer

Seconds until the write bucket refills

RateLimit-Resource
string

Which budget the numbers describe. write here.

Retry-After
integer

Seconds to wait before retrying

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.