Skip to content

OAuth apps

Build an app many venues install: authorization code with PKCE, tokens and refresh.

A key works for one venue and is copied by hand from the back office. If you are building a product that many venues connect to (a table-management system, a CRM, a marketing tool), register an OAuth app instead. Each venue's owner connects it with a click, chooses which of their venues it may use, and can disconnect it at any time.

Boozie uses OAuth 2.0 authorization code with PKCE (S256). There is no implicit grant and no client-credentials grant.

Registering an app#

Boozie registers apps for you. Contact us with:

  • the app's name, a one-line description, your homepage and a square logo URL;
  • your redirect URIs: up to 10, each matched exactly. They must be https://, or http://localhost / http://127.0.0.1 for development, at most 500 characters and without a #fragment;
  • the scopes the app needs (see Authentication). Every scope except hosts:read can be given to an app: hosts:read belongs to a key bound to one host.

You get a client id (bzc_…) and a client secret (bzs_…). Keep the secret on your server. When we roll it, the old secret keeps working for 24 hours.

1. Send the owner to Boozie#

Make a random state and a PKCE code_verifier (43–128 characters), and send the owner's browser to the back office's consent page:

https://admin-dev-c830.up.railway.app/oauth/authorize
  ?response_type=code
  &client_id=bzc_…
  &redirect_uri=https://yourapp.example/oauth/callback
  &scope=venue:read%20bookings:read
  &state=…
  &code_challenge=…
  &code_challenge_method=S256

code_challenge is BASE64URL(SHA256(code_verifier)) without padding. Leave scope out to ask for all of your app's scopes. https://api-dev-799b.up.railway.app/public/v1/oauth/authorize takes the same query and redirects to that page, if you'd rather keep one base URL.

import crypto from "node:crypto";

const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
const state = crypto.randomBytes(16).toString("base64url");
// Keep verifier and state in the user's session, then redirect.

The owner signs in to the back office and sees Connect an app: your app, the scopes it asks for and their venues. An owner connects their own venue. An organisation admin can connect several of the organisation's venues at once. Only owners can connect apps; managers and staff can only press Deny.

2. Handle the callback#

On Allow, Boozie redirects to your redirect URI:

https://yourapp.example/oauth/callback?code=bzac_…&state=…

Check that state matches the one you stored. The code is valid for 10 minutes and works once: the first exchange spends it, even one that fails.

If the owner declines or something is wrong with the request, you get an error instead:

errorWhy
access_deniedThe owner (or a manager) pressed Deny.
invalid_requestPKCE missing or not S256, a bad challenge, or a missing parameter.
invalid_scopeA scope your app isn't registered for.
unsupported_response_typeresponse_type wasn't code.

An unknown client_id or an unregistered redirect_uri never redirects: the owner sees an error page at Boozie, because the redirect can't be trusted.

3. Exchange the code for tokens#

curl -X POST https://api-dev-799b.up.railway.app/public/v1/oauth/token \
  -u "bzc_…:bzs_…" \
  -d grant_type=authorization_code \
  -d code=bzac_… \
  -d redirect_uri=https://yourapp.example/oauth/callback \
  -d code_verifier=…

The body is form-encoded. Authenticate with HTTP Basic as above, or send client_id and client_secret as form fields.

{
  "access_token": "bz_at_test_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "bz_rt_test_…",
  "scope": "venue:read bookings:read",
  "venue_ids": [2]
}

Store both tokens against the installation, with venue_ids.

4. Call the API#

An access token works exactly like a key:

curl https://api-dev-799b.up.railway.app/public/v1/venues \
  -H "Authorization: Bearer bz_at_test_…"

curl "https://api-dev-799b.up.railway.app/public/v1/venues/2/bookings?updatedSince=2026-11-07T00:00:00%2B08:00" \
  -H "Authorization: Bearer bz_at_test_…"
  • The venue is in the path. GET /venues lists the venues the token covers, each with developerAccess. A venue the token doesn't cover is 404.
  • What a token can do is the installation's scopes ∩ your app's scopes ∩ the venue's plan. A scope the venue's plan lacks gets 403 auth.plan_feature at that venue only.
  • Writes act as the owner who connected the app. Bookings you make have the source api, and the venue's audit log names your app.
  • A venue whose Developer access Boozie has turned off answers 403 auth.developer_access_off until it is back on.
  • /hosts/me… isn't available to OAuth tokens: they can't hold hosts:read, so the answer is 403 auth.scope_missing.
  • Webhooks are per venue: with webhooks:manage, register an endpoint at /venues/{venueId}/webhooks for each venue.
  • Rate limit: each installation may make 120 requests a minute unless Boozie set another limit for your app. All of the installation's venues share it. Over it the answer is 429 rate_limited with Retry-After.

5. Refresh#

Access tokens last an hour. Before one runs out, or when you get a 401, refresh it:

curl -X POST https://api-dev-799b.up.railway.app/public/v1/oauth/token \
  -u "bzc_…:bzs_…" \
  -d grant_type=refresh_token \
  -d refresh_token=bz_rt_test_…

Refresh tokens rotate. Each one is valid for 90 days and works once: the answer has a new refresh token, and you must store it before using the new access token. Using an old refresh token again is treated as theft: every token of that grant (the refresh token's line, back to the code it came from) is revoked and you get invalid_grant. The owner then has to connect the app again.

If two of your workers can refresh the same installation, put a lock around the refresh: two refreshes with the same token count as reuse.

Revoking and disconnecting#

  • Your app can revoke a token (RFC 7009). The answer is always 200, also for a token that is unknown or not your app's:

    curl -X POST https://api-dev-799b.up.railway.app/public/v1/oauth/revoke -u "bzc_…:bzs_…" -d token=bz_rt_test_…
    

    Revoking a refresh token ends the grant: it and every access token issued from it stop. Revoking an access token stops only that token; the refresh token still works.

  • An owner disconnects your app in the back office under Developers → Connected apps. Every token stops at once, and the next refresh gets invalid_grant.

  • If Boozie disables your app, its tokens stop and the token endpoint answers invalid_client.

Token endpoint errors#

The token and revoke endpoints follow RFC 6749, not the API's problem details:

{ "error": "invalid_grant", "error_description": "the code is unknown, expired or already used" }
StatuserrorTypical cause
400invalid_requestA missing or repeated parameter.
401invalid_clientThe client is unknown, switched off, or its secret is wrong. The answer has WWW-Authenticate: Basic.
400invalid_grantSee below.
400unsupported_grant_typeNot authorization_code or refresh_token.

invalid_grant covers every code or refresh token that can't be used:

  • A code that is unknown, expired or already used. If a used code comes back, Boozie also revokes the tokens it issued, because someone else may have it.
  • A wrong code_verifier. The code is spent all the same; start again from step 1.
  • A redirect_uri that doesn't match the authorization request, or a code issued to another client.
  • A refresh token that was used, expired or revoked, or an installation the owner disconnected.

error_description says which, in words meant for your logs.

Test and live#

Tokens carry the environment's mode, like keys: bz_at_test_… on the sandbox, bz_at_live_… on production. A token used on the other environment gets 401. Your app is registered separately on each.

Next: Organisations