Skip to main content

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 new refresh_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 with GET /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.