API Keys

Create, rotate, revoke, and store BetterSuite tenant API keys (btk_*) for server-to-server access.

Last updated May 19, 2026

API keys identify a tenant, not a user. They're the right answer for any backend or scheduled job calling the BetterSuite GraphQL API on the tenant's behalf. For user-bound calls (a passenger placing a ride, a driver going online), use a JWT session instead.

Two key kinds

BetterSuite issues two distinct kinds of tenant-identifying credentials. Don't mix them up.

KindPrefixSecret?Use
Tenant API keybtk_Yes — store like a passwordServer-to-server. Authorises any tenant-scoped operation the caller's role allows.
Public client keybpk_No — safe to embed in clientsIdentifies the tenant on pre-auth / public endpoints (e.g. branding lookup, login forms). Cannot perform privileged actions.

This page is about btk_ keys. The bpk_ key is auto-generated when your tenant is provisioned and lives on the tenant record — you don't issue or rotate it by hand.

When to use a tenant API key

  • A backend service calling BetterSuite (your server -> our GraphQL).
  • CI jobs, batch imports, data exports.
  • Webhook handlers calling back into BetterSuite to fetch additional context.

Don't use them in browser or mobile-app code — they'd leak into network traces. Either mint a short-lived JWT for the user, or proxy through your own backend.

Creating a key

Owner Dashboard -> API Keys -> Create Key.

Issuing a key is an Owner-tier action — a Tenant Admin role can't do it alone. The mutation that powers the dashboard, createApiKey, calls req_ctx.is_owner_tier() and rejects anything below TenantOwner (or PlatformAdmin impersonating).

Fields the dashboard accepts:

FieldNotes
NameRequired. Unique per tenant; you can't reuse a name on an active key.
Rate limit (req/min)Optional override. Blank inherits the tenant default.
Rate limit (req/day)Optional override. Blank inherits the tenant default.

The mutation supports an expiresAt timestamp and a scopes array, both currently optional and exposed via the GraphQL API but not surfaced in the dashboard UI. scopes is reserved for future fine-grained permissions.

Key format

btk_<tenant-prefix>_<random>
  • btk_ — fixed prefix.
  • <tenant-prefix> — first 8 characters of your tenant UUID, useful for grepping logs.
  • <random> — 32 cryptographically random bytes, base64url-encoded (no padding).

Total length is ~57 characters. We store only the SHA-256 hash of the plaintext in the database, plus a 12-character display prefix (btk_ + the 8-char tenant prefix) so the dashboard can show "which one is this." Once you close the create dialog, the plaintext is unrecoverable — rotate if lost.

Sending the key

Use the X-Api-Key header:

POST /graphql HTTP/1.1
Host: api.bettersuite.io
X-Api-Key: btk_019234ab_Kj8mN2pQrStUvWxYz1...
Content-Type: application/json

{"query": "{ tenantInfo { id name } }"}

If both Authorization: Bearer and X-Api-Key are present, the JWT wins — the middleware checks the Authorization header first.

What the server resolves from a key

When the middleware accepts your key, it sets x-tenant-id on the inbound request from the key's tenant_id column. That's it — no account_id, no role, no session_id. Resolvers see a tenant-only RequestContext with account_id = None. Any resolver that requires a user (mySavedLocations, me, etc.) will reject.

Each accepted call asynchronously bumps last_used_at on the key row, so the dashboard "Last used" timestamp is reasonably fresh.

Rotation

The dashboard's Rotate button issues a fresh key and revokes the old one in a single mutation. Rotation requires a step-up confirmation — you'll be prompted to re-enter your password, the dashboard receives a 5-minute elevation token, and the mutation only succeeds with that token attached as X-Elevation. The reasoning lives in the resolver comment: rotation is destructive enough that a hijacked Owner session shouldn't be able to quietly mint a long-lived backend key.

The new key inherits the old key's name, expiry, scopes, and rate-limit overrides. You can override the name and expiry on rotate if you need to.

Recommended rollout:

  1. Click Rotate, re-enter your password, copy the new plaintext.
  2. Deploy the new key to your secrets manager.
  3. Cycle the service so it picks up the new value.
  4. Wait one full deploy cycle so in-flight retries land on the new key. The old key has already been revoked at the database level, so any retry on it returns 401 API key is revoked or expired.

Revoking

Revoke is the destructive button. Once a key is revoked, every subsequent request using it returns 401 API key is revoked or expired. There is no undo — re-create if you need it back.

Revoke immediately if:

  • The key leaks into a public repo, a build log, or a screenshot.
  • A team member with access leaves.
  • A laptop or CI runner that held the key is compromised.

There's also a revokeAllApiKeys mutation that wipes every key for the tenant in one shot. It's an Owner-tier action exposed via the API for incident response; the dashboard doesn't currently surface it as a button.

Storage

  • Use a secrets manager — AWS Secrets Manager, HashiCorp Vault, Doppler, 1Password Connect, Doppler, GCP Secret Manager. Anything that isn't a committed env file.
  • Add the key to your logger's redaction list. A btk_ prefix is easy to grep for; if you find one in a log, treat it as leaked and rotate.
  • Use a separate key per environment (dev, staging, prod). They aren't pre-fixed as live/test today — the differentiator is "which tenant the key belongs to," so create separate tenants for staging and prod if you want hard isolation, and create distinct keys per environment within each.

Audit

Every key creation, rotation, revocation, and bulk-revoke writes two entries: a row in the API-key-specific audit table (used by support for forensics), and an audit_events row attributed to the actor (used by the tenant Audit Log page in the Owner Dashboard). The API exposes the key-specific log via the apiKeyAuditLog query for tooling.

What's next

Build the foundation once. Expand without limits.

BetterSuite is built for teams who see on-demand as a business — not a feature.