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

# Authentication

> Partner API key, user identification, and what to design around.

FanFeed **trusts your authentication.** There is no FanFeed login and no OAuth handshake, and
FanFeed never sees an end-user token. Two things identify a request:

* **`X-PARTNER-API-KEY`**, your partner key. Identifies your organization.
* **`user_id`** in the path. Identifies which of your users the request concerns.

You authenticate your own users however you already do. FanFeed takes your word for it.

Everything the key touches is scoped to it. Profiles are per-partner: creating a user with an
email that already exists under another FanFeed partner creates a separate, independent
profile, and no call made with your key can return media or history your integration did not
create.

### Proxy the ingest through your backend

The API key is a **server-side secret**. It authorizes access to every one of your users'
data, so anything holding it can read any of them. It must not ship inside your mobile app.

That is straightforward for four of the five endpoints. The awkward one is the camera-roll
ingest, which naturally originates on-device. Route it **device → your backend → FanFeed**:
your backend holds the key and passes each batch through. That costs you one passthrough
endpoint and the bandwidth of the metadata, which is JSON only. No image bytes ever cross the
wire, so a full 200-item batch is a few tens of kilobytes.

<Note>
  **Settled 2026-08-27.** The proxy is the agreed shape and the one to build against.
  Short-lived per-user scoped tokens were considered and are **not planned**: there is no
  token-issuance endpoint and none is scheduled. If your architecture cannot proxy, raise it
  before you build. It is a contract addition and needs a decision.
</Note>

### Keys, environments, and rotation

* A key looks like `<partner>-<secret>`. The part before the first hyphen is your partner id;
  send the key **whole** in `X-PARTNER-API-KEY`.
* **Each key is bound to exactly one environment.** A sandbox key against production is a
  `401`, and so is the reverse. You will be issued two.
* We store only a hash of the key, so a lost key is replaced rather than recovered.
* **Rotation is coordinated.** There is no self-service rotation endpoint. We issue the new
  key, both work during an overlap window you choose, then the old one is revoked. Ask
  before you need it, not during an incident.

### Rate limiting

Nothing is throttled today, but the limiter exists. When limits are enabled they are set per
key and per environment, split into three independent budgets over a rolling window:

| Budget   | Endpoints                                                   |
| -------- | ----------------------------------------------------------- |
| `ingest` | `POST /users/{user_id}/media`                               |
| `read`   | `GET /users/{user_id}/events`, `GET /users/{user_id}/stats` |
| `write`  | `POST /users`, `POST /users/{user_id}/sync-complete`        |

Exhausting one does not affect the others: if ingest is throttled, you can still read the
history you have already built. Exceeding a budget returns `429` with `rate_limited` and a
`Retry-After` in seconds. Honor it rather than backing off on a fixed schedule; see
[Errors](/conventions#errors).

`GET /v1/health` reports the limits in force for your key and is never throttled, so it answers
even while you are being limited; see
[Operational endpoints](/conventions#operational-endpoints).

Sandbox and production keys carry separate budgets. Ask us to enable a small sandbox limit if
you want to exercise your backoff path; we can turn it on and off without a deploy or a change
to this contract.
