# Authentication and keys

> API keys, scopes, the device flow, rate limits, and OAuth for MCP.

The REST API uses API keys. A key is a bearer token that acts as the account that made it, limited to its scopes and to `/v1`. The MCP server uses OAuth instead, so your AI client never holds a long-lived secret.

## API keys

Keys start with `rl_live_` and go in the header: `Authorization: Bearer rl_live_...`. Only a hash is stored, so you see a key once. A key works on every workspace its owner can edit; pass `workspaceId` to target one other than the personal workspace. Outside `/v1` keys get a 403 with code `api_key_scope`, so a leaked key cannot read the rest of the account. A call the key's scopes do not cover gets a 403 with code `api_key_missing_scope`.

### Scopes

| Scope | Unlocks |
| --- | --- |
| `productions` | `/v1/productions` and `/v1/webhooks`: productions and their webhooks. |
| `renders` | `/v1/renders`: the older one-call Render API (images in, video out). |
| `capture` | `/v1/capture`: uploads from Capture for Mac. |
| `import` | `/v1/import`: After Effects imports from the panel. |

App keys carry the scopes you tick. Device-flow keys carry all four, since the owner approves them for their own account and a second pairing for one more scope would add nothing.

## The device flow

For CLIs, coding agents and anything else without a browser session. Starting needs no key; a human approves once in a browser.

1. **Request.** `POST /v1/keys/request` with a `name` that says who is asking (tool plus machine or project). The approver sees it.
2. **Show the approval URL.** The 201 has `approvalUrl` (`https://www.raylight.app/keys/approve?code=XXXXXXXX`) and `userCode`. Have your user open it and click Approve; they can sign in or create an account from that page. The request expires after 10 minutes (`expiresInSeconds`).
3. **Poll.** `POST /v1/keys/poll` with the `deviceCode` every `pollIntervalSeconds` (3). `pending` until approved.
4. **Claim the key.** The first poll after approval answers `approved` with `apiKey`, once, and the server wipes the plaintext. Later polls answer `claimed`. Save the key before doing anything else.
5. **Terminal states.** `denied`: the user refused. `expired`: the 10 minutes passed, or the key was approved but not claimed within 10 minutes (it is revoked and the plaintext cleared). Start a new request.

Start:

```bash
curl -X POST https://api.raylight.app/v1/keys/request \
  -H "Content-Type: application/json" \
  -d '{ "name": "CI on acme/web" }'
```

201:

```json
{
  "deviceCode": "1aSs9fWUrhEUu3hOo5TzSJfWDgradXEK_k9BIV67XV8",
  "userCode": "ABCD2345",
  "approvalUrl": "https://www.raylight.app/keys/approve?code=ABCD2345",
  "expiresInSeconds": 600,
  "pollIntervalSeconds": 3
}
```

Poll:

```bash
curl -X POST https://api.raylight.app/v1/keys/poll \
  -H "Content-Type: application/json" \
  -d '{ "deviceCode": "1aSs9fWUrhEUu3hOo5TzSJfWDgradXEK_k9BIV67XV8" }'
```

Answers:

```json
{ "status": "pending" }
{ "status": "approved", "apiKey": "rl_live_..." }
{ "status": "claimed" }
{ "status": "denied" }
{ "status": "expired" }
```

> **Lost the claim response?** If the poll carrying the key timed out on your side, the key exists but you never saw it. Poll again with `{ "deviceCode": "...", "revokeClaimed": true }`: that key is revoked, the poll answers `expired`, and you start a fresh request. The flag only works for about 15 minutes after the claim window, so an old device code from a log cannot revoke anything later.

`/v1/keys` allows 60 requests a minute per IP, plenty for one poll every three seconds.

## Key management in the app

Raylight API, then Keys. Create a key with a name and scopes, see each key's prefix, scopes, creation date and last use, and revoke any key. Revocation is immediate and keeps the key's history. A key cannot be shown again; make a new one.

### Revoke the key you are using

`DELETE /v1/keys/current` with the key in the `Authorization` header revokes that key and answers 204. It needs no scope (30 calls a minute per key), so any key can retire itself: a tool that is done with a machine, or that thinks its key leaked, can sign out without a person opening the app. Only an API key can revoke itself; a browser session gets 400 `not_api_key`. `raylight logout` makes this call before deleting the saved key, and the SDK has it as `raylight.keys.revokeCurrent()`.

## Rate limits

Fixed one-minute windows. Over the limit you get 429 with `{ "code": "rate_limited" }` and a `Retry-After` header in seconds.

| Surface | Limit | Counted per |
| --- | --- | --- |
| `/v1/productions` | 120 a minute | key |
| `/v1/webhooks` | 60 a minute | key |
| `/v1/renders` | 30 a minute | key |
| `/v1/keys` | 60 a minute | IP |
| `/production-links` | 30 a minute | IP |

The monthly pitch cap (per workspace) is separate and return 429 with `code: "cap"`. See [Limits and pricing](https://www.raylight.app/docs/limits.md).

## MCP uses OAuth, not keys

The MCP server at `https://api.raylight.app/mcp` uses OAuth 2.0 with PKCE. Your AI client opens a Raylight page that shows the app asking, the scopes, and where you will be sent back. You approve, and the client gets a token for your account that refreshes on its own. API keys do not work there, and MCP tokens do not work on `/v1`. Setup for every client is on [/mcp](https://www.raylight.app/mcp).

## More

- Previous: [Webhooks](https://www.raylight.app/docs/webhooks.md)
- Next: [SDK](https://www.raylight.app/docs/sdk.md)
- All docs: https://www.raylight.app/llms.txt
- Everything in one file: https://www.raylight.app/llms-full.txt
- OpenAPI: https://api.raylight.app/v1/openapi.json
