Authentication
The Service API authenticates requests with an API key, sent as a
Bearer token in the
Authorization header.
GET /v1/reservations HTTP/1.1Host: api.useservice.appAuthorization: Bearer sk_live_YOUR_SECRET_KEYWith curl:
curl https://api.useservice.app/v1/reservations \ -H "Authorization: Bearer $SERVICE_API_KEY"The examples in these docs read the key from a SERVICE_API_KEY environment
variable rather than hard-coding it — keep your real key out of shell history,
scripts, and source control.
How keys work
Section titled “How keys work”- A key belongs to one restaurant. It authenticates as that restaurant and only ever returns that restaurant’s data, which is why no request carries a restaurant id. There is no group-level credential: a ten-venue group holds ten keys, one per venue, and every call is about a single venue.
- Keys are secret. A key beginning with
sk_live_carries whatever scopes the restaurant granted it, which can include booking, changing and cancelling on that restaurant’s behalf. Treat it like a password. - Keys are server-side only. Never embed a key in a browser, mobile app, public repository, or any client-side code. Use it only from your backend — see There is no browser-safe key.
- Keys are shown once. The full secret is displayed a single time, when the key is created. Only a short suffix (the last 4 characters) is stored and shown afterwards, so store the full value securely on creation. If you lose it, create a new key.
- Multiple keys are supported. A restaurant can hold several keys (for example, one per integration), so you can rotate or revoke one without disrupting the others.
Getting a key
Section titled “Getting a key”API keys are self-serve from the back office. An owner or co-owner can manage them under Settings → Developers:
- Open Settings → Developers and create a new API key.
- Choose the scopes it should carry. Grant the fewest that the integration needs; you cannot add one later.
- Copy the secret (
sk_live_…) — it is shown once, at creation. Store it somewhere secure before you leave the page. - Send it as a Bearer token, as shown above.
Each key is pinned to an API version at creation and can be revoked at any time from the same screen. A restaurant can hold up to 10 keys — keep one per integration so you can rotate or revoke individually.
There is no separate sandbox or test environment: all keys are live
(sk_live_…) and work against the real restaurant over the single production
base URL. A booking you create is a
booking the restaurant will set a table for, and a cancellation reaches the
guest. Rehearse against a restaurant you own.
Scopes
Section titled “Scopes”A key does not automatically carry every permission the restaurant has. Each one
is minted with a list of scopes, chosen at creation, and it cannot widen that
list afterwards — to grant more, mint a second key. A key created before scopes
existed carries reservations:read, and that is also what a new key defaults to.
| Scope | What a key holding it may do |
|---|---|
reservations:read |
Read reservations and their lifecycle events, availability, the booking policy, the seating areas, holds, and the waitlist queue. |
reservations:write |
Book, change and cancel. Also place and release holds: there is no separate hold scope, because a key that may book may hold. |
reservations:write:override |
Three things, on one scope. Send override, which books past a rule the restaurant set. Send excluded_from_shift_limits: true, which keeps a booking off the service’s covers cap for good. Send entire_section_id, which books every table in a section, including a section the restaurant keeps off online booking. See Book a whole room. |
guests:read |
Read guest records, expand a guest from a reservation, expand the modification token on POST /v1/reservations, and read the guest data on a booking — on every booking, including the ones your key made. A modification token opens the guest’s booking page as the guest, card step included, so treat it like their password. |
Scopes do not nest. reservations:write does not imply reservations:read, and
reservations:write:override does not imply reservations:write — ask for each
one the integration uses. reservations:write:override needs
reservations:write beside it, and is deliberately separate from it: it lets a
restaurant hand you the ability to book without handing you the ability to book
past its own rules, to keep a booking off its covers caps, or to take a room off
sale.
Those three powers were three scopes until 2026-09-22. They are one now. The
three fields stay apart — each is refused on its own, naming its own param —
but a restaurant granting any of them is saying one sentence, and a key that
carries the scope carries all three.
Some scopes are asked for by a field, not by an endpoint
Section titled “Some scopes are asked for by a field, not by an endpoint”reservations:write:override and guests:read can be demanded by something
inside the request rather than by the endpoint you called. On a create, an
override array, excluded_from_shift_limits: true and entire_section_id each
ask for the first; expand=guest asks for the second, whichever endpoint you
reached it from.
The refusal tells you which of the two happened. A 403 insufficient_scope that
names a param means the call is in reach and only that one field is not, so
dropping the field and retrying is worth automating. Without a param, the key
cannot reach the endpoint at all and retrying changes nothing.
Errors is the reference: the scope each endpoint needs, the fields that demand one, and the refusal envelope.
Guest data on a booking
Section titled “Guest data on a booking”A reservation carries the contact details given for that booking
(contact_email, contact_phone), the guest’s own request (guest_notes) and
the restaurant’s private note about it (internal_notes). These four fields are
guest data, and guests:read is the one thing that reads them. A key holding it
reads them on every booking, as they stand, so a later change by the
restaurant’s staff, or by the guest from their booking page, reaches it too.
A key without the scope reads none of them, on any booking — the bookings it created itself included, and the response to the create that made them. Echoing back what you just sent would tell you nothing new; reading the same booking an hour later would, which is why your own bookings are not an exception.
Where they are withheld the four fields are absent from the response, not
null. null already has a meaning on them: contact_email: null says the
booking follows the guest profile’s contact. Test whether the key is present
before you read its value. Nothing is refused: there is no 403, and the rest
of the booking is complete.
The rule holds wherever a booking is rendered: a list, a single read, the
response to a create, a PATCH or a cancel, and the changes of a
reservation’s events. A PATCH from a key
without guests:read writes guest_notes and internal_notes and answers
without them. A waitlist entry’s notes, the guest’s own request, are gated the
same way.
internal_notes is the restaurant’s own note rather than the guest’s words, and
you write it as well as read it: it is a field of the create and of the PATCH,
the same note the restaurant’s staff type on their own booking form. A waitlist
entry’s private note is on no partner surface.
A guest record’s notes are a different field: the notes the restaurant’s staff
keep on the guest profile. The guest does not write them. They reach you
wherever the guest record does: GET /v1/guests, expand=guest and the
guest.* events.
A webhook endpoint is not tied to a key, and its deliveries carry guest data in full. See Deliveries carry guest data.
There is no browser-safe key
Section titled “There is no browser-safe key”Every credential this API issues is secret. There is no publishable variant, and
api.useservice.app answers no cross-origin request from an origin you control,
so a key cannot work from a page you ship to guests even if you were willing to
put it there.
A booking funnel you build yourself therefore needs a server of your own: the guest’s browser talks to your backend, and only your backend holds the key. For a booking flow that runs entirely in the browser with no backend at all, use the Booking Widget instead — it needs no key.
Authentication errors
Section titled “Authentication errors”A missing, malformed, revoked, or unknown key returns 401 Unauthorized with an
error envelope. There are two authentication codes:
auth_header_missing— noAuthorization: Bearer …header was sent.invalid_token— a key was sent but is invalid, revoked, expired, or unknown.
{ "error": { "type": "authentication_error", "code": "invalid_token", "message": "The provided API key is invalid, revoked, or expired.", "doc_url": "https://docs.useservice.app/api/errors#invalid_token" }}(param is omitted here — no request parameter is at fault.)