Skip to content

Authentication

The Service API authenticates requests with an API key, sent as a Bearer token in the Authorization header.

GET /v1/reservations HTTP/1.1
Host: api.useservice.app
Authorization: Bearer sk_live_YOUR_SECRET_KEY

With curl:

Terminal window
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.

  • 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.

API keys are self-serve from the back office. An owner or co-owner can manage them under Settings → Developers:

  1. Open Settings → Developers and create a new API key.
  2. Choose the scopes it should carry. Grant the fewest that the integration needs; you cannot add one later.
  3. Copy the secret (sk_live_…) — it is shown once, at creation. Store it somewhere secure before you leave the page.
  4. 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.

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.

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.

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.

A missing, malformed, revoked, or unknown key returns 401 Unauthorized with an error envelope. There are two authentication codes:

  • auth_header_missing — no Authorization: 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.)