> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flynet.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Two credentials, two patterns. Choose by who you are acting on behalf of.

Flynet uses two credentials for partner integrations. Which one
you use depends on which routes you call.

## Pick by route

| Route family                                                     | Credential                                              |
| ---------------------------------------------------------------- | ------------------------------------------------------- |
| `/restaurants*` and `/locations*`                                | **API key** in `X-API-Key`                              |
| `/check_ins` (venue feed) and `/check_ins/{id}`                  | **API key** in `X-API-Key`, minted with `read:checkins` |
| `/users/me/*` (profile, status, wallets, check-ins, 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` (or the `/check_ins` venue feed) returns an empty-body `401`
— no API key present. Sending an API key to `/users/me/check_ins` is
rejected the same way: a member's own history always needs their token.
Both return `401` with the reason in the `WWW-Authenticate` header, not
JSON.

<Tip>
  **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.
</Tip>

## What each credential authorizes

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         | None at v1                                 |
| **OAuth access token** | The authenticated member's own profile, status, wallets, tags, check-ins, and memberships | Payment Intents on behalf of your merchant |

### OAuth scopes

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:user_checkins` | `/users/me/check_ins`           |
| `read:wallets`       | `/users/me/wallets`             |
| `read:tags`          | `/users/me/tags`                |
| `read:memberships`   | `/users/me/memberships`         |

These are OAuth (JWT) scopes, requested at `/authorize`. The
`/check_ins` venue feed and `/check_ins/{id}` are **API-key** routes
gated by the separate `read:checkins` scope minted on the key — not an
OAuth scope. See [API keys](/concepts/api-keys).

The member who completed the OAuth flow is the token's `sub` claim.
See [Identify the authenticated member](/concepts/oauth#identify-the-authenticated-member).

## Credential lifetimes

| Credential          | Lifetime                            | Refresh path                                                                                              |
| ------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| API key             | Doesn't expire until revoked        | [Support](/resources/support) to revoke + reissue                                                         |
| OAuth access token  | 60 minutes                          | `POST /oauth/token` with `grant_type=refresh_token`; see [OAuth Step 4](/concepts/oauth#step-4---refresh) |
| OAuth refresh token | Up to 30 days, rotated on every use | Same call rotates it                                                                                      |

## How credentials reach you

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](/concepts/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 `fly_test_` or `fly_live_` prefix | Discovery routes (`/restaurants*`, `/locations*`). Send as `X-API-Key`. Backend only. `fly_live_` is a production key, `fly_test_` a staging/dev key. 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:user_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](/concepts/oauth) for the
full OAuth flow and [API keys](/concepts/api-keys) for
server-to-server use.

<Warning>
  **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.
</Warning>

## 401 behavior

The 401 envelope is determined by the **route family's gating
filter**, not by which credential you happened to send.

* **OAuth-protected routes** (`/users/me/*`, `/payment_intents/*`)
  return **HTTP 401 with an empty body** on missing, malformed, or expired
  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` venue feed, `/check_ins/{id}`)
  return the **same empty-body 401** when the API key is missing. An *invalid*
  (expired or revoked) key is the one case that returns a JSON body — envelope A
  with `code: "invalid_api_key"`. See [Pagination + errors](/concepts/pagination-errors)
  for the exact shapes.

See [Debugging](/resources/debugging) for every observed `error_code`
mapped to cause and fix.

## Choose your on-ramp

<CardGroup cols={2}>
  <Card title="Hello API Key" icon="key" href="/recipes/appetizers/hello-api-key">
    One curl, one API key, confirm Discovery works.
  </Card>

  <Card title="Hello OAuth" icon="user" href="/recipes/appetizers/hello-oauth">
    One curl, one access token, confirm a member route works.
  </Card>
</CardGroup>
