# Authentication

Every request to `https://faceless.so/api/v1` is authenticated with an API key. Keys are created in the dashboard, in your team settings at https://faceless.so/team, and look like `fl_live_...`.

## Sending the key

Two headers are accepted; use whichever fits your HTTP client:

```bash
curl -s https://faceless.so/api/v1/me \
  -H "Authorization: Bearer $FACELESS_API_KEY"
```

```bash
curl -s https://faceless.so/api/v1/me \
  -H "X-API-Key: $FACELESS_API_KEY"
```

Keys are accepted in headers only. Never put a key in a URL or query string; URLs end up in logs.

## OAuth 2.0 for agents

Alongside API keys, `/api/v1` accepts OAuth 2.0 bearer tokens. This is the agent-native path: a client can discover everything, register itself and obtain scoped tokens without a human copying a key - only the consent screen needs a person.

Discovery starts at the standard metadata documents:

- `GET /.well-known/oauth-authorization-server` - RFC 8414 authorization server metadata (endpoints, grant types, `code_challenge_methods_supported`)
- `GET /.well-known/oauth-protected-resource` - RFC 9728 resource metadata (`scopes_supported` with descriptions)

The flow is authorization code with PKCE (S256 required), with dynamic client registration (RFC 7591):

1. `POST /api/oauth2/register` with `{ "client_name": "...", "redirect_uris": ["https://..."] }`. Clients are public by default; PKCE stands in for a secret. Loopback `http://localhost` redirect URIs are allowed for CLI and desktop agents.
2. Send the user to `/oauth2/authorize?response_type=code&client_id=...&redirect_uri=...&scope=videos:read videos:write&state=...&code_challenge=...&code_challenge_method=S256`. They pick a team and approve the scopes on the consent screen.
3. Exchange the code at `POST /api/oauth2/token` (`grant_type=authorization_code`, with your `code_verifier`).
4. Call the API with `Authorization: Bearer fl_oat_...` - exactly where an API key would go, with the scopes the user approved.

Access tokens live 1 hour; refresh tokens live 30 days and rotate on every use (`grant_type=refresh_token`). Reusing a rotated refresh token revokes the whole grant, so store the newest pair only. Revoke at `POST /api/oauth2/revoke`. Omitting `scope` grants read-only scopes; request write scopes explicitly.

API keys remain the simpler path for your own integrations: one header, no flow, no expiry. OAuth exists for clients acting on behalf of someone else's account.

Using the `faceless` CLI or the `faceless-mcp` stdio server? `faceless login` runs this whole flow for you - it opens the consent screen in your browser, stores the tokens in `~/.faceless/config.json`, and refreshes them automatically. No key to paste.

One disambiguation: elsewhere in these docs, "connecting" a platform refers to linking a **social account** (YouTube, TikTok, Instagram, X, Facebook, LinkedIn, Threads) to a team so the API can publish to it. That linking happens in the dashboard, in a browser, because each platform requires a human to grant consent on its own consent screen. `GET /accounts` then lists what is connected, and you pass those ids as `authId` when publishing.

A missing or invalid key returns `401` with error type `unauthorized`.

## Scopes

Each key carries a set of scopes chosen at creation. Calling an endpoint the key is not scoped for returns `403` with error type `forbidden_scope`.

- `credits:read`: credit balance and ledger (GET /credits)
- `videos:read`: list and read videos, poll generation status and renders (GET /videos, GET /videos/{id}, GET /videos/{id}/status, GET /renders/{id})
- `videos:write`: create, caption, update, render and delete videos (POST /videos, POST /videos/captions, PATCH /videos/{id}, POST /videos/{id}/render, DELETE /videos/{id})
- `series:read`: list and read series and their episodes (GET /series, GET /series/{id}, GET /series/{id}/episodes)
- `series:write`: create, update and delete series, force the next episode (POST /series, PATCH /series/{id}, DELETE /series/{id}, POST /series/{id}/generate)
- `posts:read`: the posting calendar (GET /calendar)
- `posts:write`: publish, schedule and cancel posts (POST /posts, POST /posts/schedule, DELETE /posts/{id})
- `accounts:read`: list connected social accounts (GET /accounts)
- `assets:read` / `assets:write`: read and register media assets (POST /assets)
- `analytics:read`: cross-platform performance (GET /analytics)
- `catalog:read`: voices and option catalogs (GET /voices, GET /options)

`GET /me` works with any valid key regardless of scopes; use it to check which scopes a key has (`data.auth.scopes`).

## Legacy team keys

Teams created before scoped keys have a legacy team API key (a UUID). It still works in the same headers and is treated as having every scope. Prefer creating a scoped `fl_live_` key: it can be revoked on its own and limits the blast radius of a leak. `GET /me` reports `data.auth.legacy: true` when a legacy key is used.

## Managing keys

Everything happens in team settings at https://faceless.so/team:

- Create a key with a name and the scopes it needs
- The full key is shown once, at creation; only a prefix (like `fl_live_a1b2c3d4`) is visible afterwards
- Revoke a key at any time; revoked keys stop working immediately

## Security notes

- Store keys in environment variables or a secret manager, never in source control
- Give each integration its own key with the minimum scopes it needs, so one leak has a small blast radius and revocation does not break everything else
- If a key leaks, revoke it in the dashboard and create a new one
- Keys act on behalf of your team; treat them like passwords
