API key in X-API-Key, minted with the read:checkins scope
/check_ins/{id}
OAuth access token in Authorization: Bearer
/memberships
OAuth access token in Authorization: Bearer
/payment_intents/*
OAuth access token in Authorization: Bearer
The two credentials are not interchangeable. Sending an OAuth
bearer to /restaurants returns 401 MISSING_API_KEY. Sending
an API key to /users/me/check_ins is rejected — a member’s own
history always needs their token.
From the kitchen - Reach for the API key first when you are
building app-level discovery, such as a map of nearby restaurants,
a catalog page, hours, or locations. Reach for OAuth when you are
doing anything with a member’s wallets, tags, check-ins, or payments.
The API key is issued per partner, per environment, and grants
app-level read access to Discovery data. The OAuth access token
acts on behalf of the member who completed the flow. Member
routes resolve the subject from the token’s sub claim, so
/users/me/* returns that member’s data, never another member’s.
Credential
Reads
Writes
API key
Any restaurant, any location, any open hours in the environment it was issued for; the anonymized check-in feed (with the read:checkins scope)
None at v1
OAuth access token
The authenticated member’s own profile, status, wallets, tags, and check-ins; the memberships list
Member routes are gated by scope. A token without the required
scope returns 403 with WWW-Authenticate: Bearer error="insufficient_scope":
the route still exists; the token simply isn’t authorized for it.
Scope
Gates
read:profile
/users/me, /users/me/status
read:checkins
/check_ins/{id}, /users/me/check_ins; also required on the API key for the /check_ins feed
read:wallets
/users/me/wallets
API keys carry scopes too: they are set in the body of the key-mint
request, and the app’s allowed_scopes are not inherited. A key
minted without read:checkins gets 403 insufficient_scope on the
/check_ins feed.The member who completed the OAuth flow is the token’s sub claim.
See Identify the authenticated member.
After your app is approved, Blackbird sends your credentials by
email — a set scoped to staging. Production credentials are
issued separately when you’re approved for live traffic; see
Environments.
Field
Example
Used for
client_id
df1f9d01-… (UUID)
OAuth /authorize and /token. Public: safe to embed in browsers when paired with PKCE.
client_secret
opaque ~40-char string
Backend /token exchange only. Never expose to a browser.
API key
40-char string with bb_test_ or bb_live_ prefix
Discovery routes (/restaurants*, /locations*). Send as X-API-Key. Backend only. The prefix is a labeling hint; env binding is server-side. Use the key you received.
API key hint
last 4 chars (e.g. mgK9)
Reference a key in support tickets without revealing the full value.
Registered redirect URI(s)
e.g. https://yourapp.com/oauth/callback
Where /oauth/authorize redirects after consent. Must match exactly. Multiple allowed.
Approved scopes
e.g. read:profile read:checkins read:wallets
The scope set your app may request. Each member route is gated by scope; a token missing the required scope returns 403 insufficient_scope.
flynet_merchant_id (if payments-enabled)
UUID
Required body field on every POST /payment_intents. Per-partner, per-env.
Allowlisted CORS origin(s)
e.g. https://yourapp.com
Browser origin(s) cleared to call the API directly. Only needed for direct browser → API.
If anything is missing or wrong, reply to the onboarding email;
fixes are fast pre-launch. See OAuth for the
full OAuth flow and API keys for
server-to-server use.
Chef’s warning - client_secret, API keys, and
flynet_merchant_id are secrets. Never expose them in client-side
code, public repos, or screenshots. client_id and registered
redirect URIs are public.
The 401 envelope is determined by the route family’s gating
filter, not by which credential you happened to send.
OAuth-protected routes (/users/*, /check_ins/{id}, /payment_intents/*)
return HTTP 401 with an empty body on missing or invalid bearer.
This also happens if you accidentally send an API key here: the
OAuth filter doesn’t see a bearer and rejects. Do not try to parse JSON.
API-key routes (/restaurants*, /locations*, the /check_ins
feed) return HTTP 401 with the MISSING_API_KEY envelope if the
API key is missing or invalid. See
Pagination + errors for the exact shape.
See Debugging for every observed error_code
mapped to cause and fix.