Business API keys
How a brand issues you a key, what the scopes mean, and how to authenticate. This is the Business API — for apps acting as one signed-in member, see the Member API authentication guide instead.
Which API you want
The Business API acts with staff authority for a whole brand. It uses a brand API key and is what you want for an integration that manages members, schedules classes, syncs data or gates door access.
The Member API acts as one signed-in member and authenticates with a Firebase ID token. If your product logs an individual member in, that is the one to read about.
Getting a key
You do not create the key — the brand does, in Hapana under Control Center → API & Integrations → API keys. Tell them which scopes you need and why; the picker shows them a description of each one before they grant it.
The key is shown once, at creation. Hapana stores only a hash, so a lost key cannot be recovered and has to be replaced. Ask the brand to paste it straight into your secret store rather than into email or chat.
| Prefix | Means |
|---|---|
hk_live_… | Production. Real members, real money. |
hk_test_… | Also issued by the brand. See below. |
The prefix is only a label. It does not change what the key can reach: a key works against the one environment it was issued in, and the host you call decides which that is. A key issued on production returns 401 on the staging host, and the other way round. You do not need a hk_live_ key to go live. Ask the brand, or your Hapana contact, for a key in each environment you build against.
Authenticating
Send the key as a bearer token. The base URL is https://public-api.hapana-app.com and every path is prefixed /public-api/v2.
curl 'https://public-api.hapana-app.com/public-api/v2/sites' \
-H 'Authorization: Bearer hk_live_…' \
-H 'User-Agent: YourProduct/1.0'Never put the key in a query string — it ends up in logs and proxy caches. Set a descriptive User-Agent; it is what we look for when helping you debug.
A key belongs to a brand, not a site. Most endpoints still take a siteId, because the data is per-location. Start from GET /sites to discover them.
Scopes
A key carries an explicit list of scopes and can do nothing outside them. Ask for the narrowest set that does your job — a key that only reads is a key that cannot damage a brand's data if it leaks.
| Scope | Allows |
|---|---|
clients:read / clients:write | List and retrieve members; create, update, merge and archive them. |
bookings:read / bookings:write | List and retrieve bookings; create, cancel, check-in and no-show them. |
sessions:read / sessions:write | List and retrieve classes; create, update and cancel them. |
packages:read / packages:write | Packages and memberships; allocate, cancel, suspend and resume a member's packages. |
checkins:read / checkins:write | List check-ins; record general and session check-ins. |
access:read / access:write | Door access: the per-site eligibility feed, tap check-ins, and the member's fobNumber and barcode, which are hidden without it. |
clients:pii | Adds dateOfBirth to member reads (GET /clients, GET /clients/{id} and GET /clients/by-barcode). Without it the field is left out. Ask for it only if you use it. |
sites:read / sites:write | Site configuration and metadata. |
staff:read / staff:write | Staff records; create, update and deactivate. |
payments:read | Payments and invoices. Read only — see below. |
reports:read | Run and retrieve reports and audit events. |
Some scopes are deliberately not grantable to any partner key, whatever a brand asks for: taking payments, issuing refunds and moving account credit; managing API keys; and creating webhook subscriptions, because an arbitrary callback URL is a data-exfiltration route. Webhook endpoints are configured by the brand in the Control Center instead.
Calling an endpoint outside your scopes returns 403 naming the scope you are missing, so the fix is always obvious.
Rate limits
Every response carries the current state of your budget:
x-ratelimit-limit: 120
x-ratelimit-remaining: 118
x-ratelimit-reset: 1790221503The budget is per brand, shared by all of its keys, and refills over a 60-second window: 120 requests a minute on the Basic plan, 600 on Premium and 1,200 on Pro. Read the headers rather than hard-coding a number. When remaining reaches zero, wait until reset (a Unix timestamp). A 429 carries a Retry-After header in seconds — wait at least that long, and back off exponentially if it repeats. See Rate limits for the detail.
Every response also carries an x-request-id. Log it. Quoting it lets us find your exact request.
Idempotency
Write endpoints accept an Idempotency-Key header. Send a unique value per logical operation — a booking attempt, a door scan — and a retry after a timeout cannot create a duplicate. This matters most where a double submission costs a member money or a class place.
When something is refused
| Status | Usually means |
|---|---|
401 | Missing, malformed or revoked key. Check the bearer prefix and the environment. |
403 with JSON | Your key lacks a scope. The body names the one required. |
403 with an HTML page | Not us — our web firewall rejected the request before it reached the API. It is triggered by values that look like an attack, and currently also by some legitimate dates and identifiers in certain field names. Quote the request to us and we will get the field allowed. |
400 | Validation. The body lists the offending field paths — check whether the field belongs in the query string or the body, as it varies by endpoint. |
429 | Rate limited. Wait for the reset timestamp. |
Looking after the key
Store it in a secret manager, never in source control or a client-side bundle — a brand key has staff authority and belongs only on your server. Use separate keys per environment and per integration, so one can be revoked without taking the others down. A brand can change a key's scopes after creation, so narrowing an over-broad key does not mean reissuing it.
