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/*, /check_ins*, /memberships, and /payment_intents/*. Use an API key for restaurant and location Discovery. 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:checkins gates /check_ins* + /users/me/check_ins, and read:wallets gates /users/me/wallets. 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. There is no user filter — the feed is anonymized; passing user= is silently ignored. (Earlier launch builds required at least one filter; that requirement was dropped.) 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, ?restaurant_id=... 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.

Why does my idempotent replay return 200 instead of 201?

The same idempotency_key on the same flynet_merchant_id returns the existing intent with HTTP 200. A new key creates a new intent with HTTP 201.