OAuth + API keys
Where do my client_id and client_secret come from?
After your app is approved, Blackbird sends them by email with your
registered redirect URI and allowed scopes.
What is the difference between OAuth and API keys?
Different routes require different credentials. Use OAuth for member-acting routes:/users/me/* (profile, status, wallets, tags,
check-ins, memberships) and /payment_intents/*. Use an API key for
restaurant and location Discovery and for the /check_ins venue feed
(including /check_ins/{id}). They are not interchangeable.
See Authentication for the per-route table.
How do I read the signed-in member’s profile, wallets, or check-ins?
Call/users/me, /users/me/wallets, or /users/me/check_ins with
the member’s OAuth access token. The subject is resolved from the
token’s sub claim, so there’s no UUID in the path. You only need to
decode sub yourself if you want the member’s UUID for your own state.
Why do I get a 403 instead of a 401 on a member route?
The token is valid but doesn’t carry the scope that route requires.
read:profile gates /users/me + /users/me/status,
read:user_checkins gates /users/me/check_ins, read:wallets gates
/users/me/wallets, and read:memberships gates /users/me/memberships.
(The /check_ins venue feed and /check_ins/{id} are API-key routes
gated by the separate read:checkins scope on the key.) Re-authorize
requesting the scope you need. The 403 body is empty; the reason is in
the WWW-Authenticate header.
Why do my refresh tokens stop working after one use?
Refresh tokens are rotated. Each successful refresh returns a newrefresh_token that replaces the previous one. Store the new token
returned by /oauth/token and use it on the next refresh.
Why does my token exchange return invalid_grant?
The most common cause is that the authorization code was consumed
before your script reached it. Authorization codes are single-use
and short-lived. If your callback URL runs auth logic automatically,
the code may already be spent.
/check_ins filters
Do I need a filter on GET /check_ins?
No. A bare GET /check_ins is valid and returns the full paginated
set. Filters ([restaurant, location, created_after, created_before]) are optional and AND together. (Earlier launch
builds required at least one filter; that requirement was dropped.)
/check_ins is an anonymized venue feed — for the authenticated
member’s own history, use /users/me/check_ins.
Why does ?created_after=1715468700 return 400?
created_after and created_before take ISO 8601 strings, not
epoch seconds. Use 2026-04-01T00:00:00Z.
Why does ?some_filter=value return unfiltered results?
Unknown query parameters are silently ignored. Check the parameter
name. For example, ?restaurants=... instead of ?restaurant=... is
silently dropped, so you get the full unfiltered set rather than an
error, which means more rows than you expected.
Payments
Why does confirm return 400 payment0030?
The customer does not hold enough FLY in their SPENDING wallet.
v1 does not card-fund or auto-load FLY. Pre-check the balance or
surface the error to the member.
Can I do partial refunds?
Not in v1. Refunds reverse the full amount of a paid intent. Partial refunds are on the roadmap.Do I get a webhook when a payment confirms?
Not in v1. Poll the intent’s status withGET /payment_intents/{id}
to see state changes.
Does reusing an idempotency_key create a duplicate intent?
No. Per the API contract, reusing the same idempotency_key returns
the existing intent rather than creating a second one. A new key
creates a new intent.