*REST API › Guides*

# OAuth

> Filmo implements OAuth 2.1 for public clients: authorization code with PKCE, no client secret. The token it issues is an API key of kind OAuth, so it works on the whole REST API and on MCP, with the scopes the administrator granted.

## Key or OAuth

| You are building | Use |
| --- | --- |
| A backend job or integration for your own company that calls the REST API on a schedule or on events | An API key, created by an administrator. See Authentication. |
| An integration that acts for many accounts, or that needs consent the customer can revoke | OAuth. Each account's administrator approves the connection and can revoke it at any time. |
| A remote MCP client, such as an assistant connector, that connects to `https://app.filmo.io/api/v1/mcp` | OAuth. The client discovers where to authenticate on its own. |

OAuth is not a second authentication system. The access token it issues is an API key of kind OAuth: it starts with `fk_live_`, travels the same way, as `Authorization: Bearer`, and works on every REST operation and on MCP. The same scopes apply, the owner is revalidated on every request, and the same rate limits count. The differences are that the token expires after 1 hour and that it is renewed with a refresh token. For simple server-to-server work, a key is simpler and is what Filmo recommends; choose OAuth when you act for many accounts or need revocable consent. The connection steps for assistants are in [MCP authentication](https://docs.filmo.io/mcp/authentication).

## How the flow works

The client holds no secret and proves possession of the authorization code with PKCE. There is no `client_credentials` grant and no confidential client: every connection needs the consent of an administrator of the account.

| Step | Endpoint | Notes |
| --- | --- | --- |
| Discovery | `GET /.well-known/oauth-authorization-server` (RFC 8414) and `GET /.well-known/oauth-protected-resource` (RFC 9728) | Both are public. The 401 from `/api/v1/mcp` carries a `WWW-Authenticate` header with `resource_metadata`, so a client finds them without configuration. |
| Registration | `POST /api/v1/oauth/register` (RFC 7591) | Open, JSON only. Public client: you receive a `client_id` and no `client_secret`. |
| Authorization | `GET /oauth/authorize` | PKCE with `S256` is mandatory. An administrator of the account sees the consent screen. |
| Token | `POST /api/v1/oauth/token` | `grant_type=authorization_code`, or `grant_type=refresh_token` to rotate. |

## Discovery

Read the metadata instead of hard-coding endpoints. Both documents are served under `/.well-known/` and can be cached for five minutes. In production the issuer, the endpoints and the protected `resource` are always on `https://app.filmo.io`: a request to `/api/v1/mcp` or to a `.well-known` document on any other production host is answered with a 308 redirect to it.

| Document | Fields |
| --- | --- |
| `/.well-known/oauth-authorization-server` | `issuer`, `authorization_endpoint` (`/oauth/authorize`), `token_endpoint` (`/api/v1/oauth/token`), `registration_endpoint` (`/api/v1/oauth/register`), `scopes_supported`, `response_types_supported` (`code`), `grant_types_supported` (`authorization_code`, `refresh_token`), `code_challenge_methods_supported` (`S256`), `token_endpoint_auth_methods_supported` (`none`), `service_documentation` |
| `/.well-known/oauth-protected-resource` | `resource` (the MCP endpoint, `https://app.filmo.io/api/v1/mcp`), `authorization_servers`, `scopes_supported`, `bearer_methods_supported` (`header`), `resource_documentation` |

The metadata has no `revocation_endpoint`, no introspection endpoint and no `response_modes_supported`. The OAuth and MCP endpoints send no CORS headers, so call these endpoints from a server, not from browser code.

## Register a client

Registration is open: it needs no credential and accepts only JSON. The client is public, so there is no `client_secret` and no registration access token.

- `redirect_uris` is required, with at most 10 entries. Each one is an absolute URI without a fragment, and is either `https`, or `http` on a loopback host such as `127.0.0.1` or `[::1]` (any port and path), or a custom scheme of a native app.
- `client_name` is optional, and a name longer than 200 characters is rejected. Other metadata you send (`grant_types`, `response_types`, `token_endpoint_auth_method`, `scope`, `logo_uri`, `software_id`) is ignored: the client is always a public client with `token_endpoint_auth_method` set to `none`.
- The answer is 201 with `client_id`, `client_id_issued_at`, `client_name`, `redirect_uris`, `grant_types`, `response_types`, `token_endpoint_auth_method` and `token_endpoint`.
- A client that is never used may be purged after 30 days, so register once and keep the `client_id`.

*Register a client*

```bash
# Register a public client once (no credentials needed). JSON only.
curl -sS -X POST "${FILMO_BASE_URL:-https://app.filmo.io}/api/v1/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{"client_name": "Acme Sync", "redirect_uris": ["https://example.com/oauth/callback"]}'
```

## Authorize

Send the administrator's browser to `/oauth/authorize` on `https://app.filmo.io`. It reads these parameters and ignores any other.

| Parameter | Rule |
| --- | --- |
| `client_id` | Required. The registered client. |
| `redirect_uri` | Required. Must be exactly equal to one of the URIs you registered: no wildcards, and no flexible port for loopback. |
| `response_type` | Must be `code`. |
| `scope` | Optional. Space-separated permissions to pre-select on the consent screen. Unknown scopes are ignored. |
| `code_challenge` | Required. |
| `code_challenge_method` | `S256` only; `plain` is rejected. |
| `state` | Optional, returned unchanged to your `redirect_uri`. |

Errors that happen before Filmo can trust the `redirect_uri` (an unknown client or a `redirect_uri` that does not match) are shown on the Filmo page and are not redirected. Any later error is sent to your `redirect_uri` as `error`, `error_description` and `state`. The authorization code is always delivered in the query string, never in a fragment.

The code is valid for 60 seconds and can be used once. Its `code_verifier` must be 43 to 128 characters long.

## Consent

Only an administrator can grant a connection. The consent screen lets the administrator choose the subscription the connection will use, and the scopes to grant: grant the minimum the integration needs. Only read scopes are pre-selected: the ones the client asks for, or all read scopes if it asks for no read scope. Write scopes, [`workflows:run`](https://docs.filmo.io/rest-api/guides/scopes#workflows:run) and [`ai:ask`](https://docs.filmo.io/rest-api/guides/scopes#ai:ask) always start unselected, even if the client asks for them; those it asked for carry the label *Lo pide la integración* (asked for by the integration), and the administrator turns them on by hand. The token is bound to that subscription, and everything the connection writes is attributed to that administrator, exactly as with a key. The connection is recorded in the activity log of the account.

Approve a connection only if you started it yourself, and check the name of the client and the host it redirects to. A connection that was granted fewer scopes than it asked for works with the ones it has: read `scope` in the token response.

## Get and refresh tokens

Post to `/api/v1/oauth/token` as a form (`application/x-www-form-urlencoded`) or as JSON. The client is public: send `client_id`, and no secret.

- `grant_type=authorization_code` requires `redirect_uri`, `code` and `code_verifier`.
- `grant_type=refresh_token` requires `refresh_token`.
- Any other grant type is rejected with `unsupported_grant_type`. There is no `client_credentials`.
- `scope` and `resource` on the token request are ignored: the scopes are the ones granted on the consent screen.

*Exchange a code and refresh*

```bash
# Exchange the authorization code (all values come from your own flow).
curl -sS -X POST "${FILMO_BASE_URL:-https://app.filmo.io}/api/v1/oauth/token" \
  -d grant_type=authorization_code \
  -d client_id="$FILMO_OAUTH_CLIENT_ID" \
  -d redirect_uri="https://example.com/oauth/callback" \
  -d code="$AUTH_CODE" \
  -d code_verifier="$CODE_VERIFIER"

# Later, rotate: send the latest refresh token and keep the new pair.
curl -sS -X POST "${FILMO_BASE_URL:-https://app.filmo.io}/api/v1/oauth/token" \
  -d grant_type=refresh_token \
  -d client_id="$FILMO_OAUTH_CLIENT_ID" \
  -d refresh_token="$REFRESH_TOKEN"
```

The answer is 200 with `Cache-Control: no-store`:

```json
{
  "access_token": "<the access token, starts with fk_live_>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<the refresh token>",
  "scope": "<the scopes actually granted, space-separated>"
}
```

### Lifetime and rotation

- The access token expires after 1 hour. After that, calls answer 401 [`api_key_expired`](https://docs.filmo.io/rest-api/errors/api_key_expired) until you refresh.
- The refresh token lasts 30 days, counted from the last rotation. Each refresh issues a new one, so a connection that is refreshed at least every 30 days stays alive.
- Rotation rewrites the same connection: the previous access token stops working at once, and the connection remains a single entry in the API and MCP section of the settings.
- If you send a refresh token that was already rotated, within 10 seconds and from the same client, you get the same new pair again, which protects you from a retry after a lost response. Later, the reuse is treated as a leak: the whole chain is revoked and you get `invalid_grant`.
- Keep the latest refresh token and let only one worker refresh at a time.
- Before rotating, Filmo checks that the administrator who consented is still an active administrator. If not, the refresh fails with `invalid_grant`.

## Use the token

Send it as `Authorization: Bearer` to any REST operation or to the MCP endpoint. The scopes are the ones the token response lists, and the key owner is checked on every request, as for a key: if the administrator who consented leaves or stops being an administrator, calls fail with 401 [`api_key_owner_inactive`](https://docs.filmo.io/rest-api/errors/api_key_owner_inactive) or [`api_key_owner_not_admin`](https://docs.filmo.io/rest-api/errors/api_key_owner_not_admin). A 401 from the REST API does not carry a `WWW-Authenticate` header; only the 401 of the MCP endpoint does, and it names the read scopes in `scope`, so a client asks for read access by default.

RFC 8707 resource indicators are not supported. A `resource` parameter on the authorization or token request is ignored, and the token is not bound to an audience.

## Revoke a connection

There is no revocation endpoint (RFC 7009). The administrator revokes the connection in the API and MCP section of the settings (*Ajustes → API y MCP*), where it is labeled as a connected integration. Access ends immediately, and a refresh from then on fails with `invalid_grant`, so the user has to give consent again. See [Authentication](https://docs.filmo.io/rest-api/guides/authentication#revoke). Connections count in the total of 50 keys that are not revoked per subscription, but only creating a key by hand is refused at the limit.

## OAuth errors

The OAuth endpoints do not use the envelope of the rest of the API, and they do not answer in `application/problem+json`. They answer in the flat OAuth 2.1 format, which is what standard clients expect. The `error_description` is always in Spanish: branch on `error`, never on the text.

```json
{ "error": "invalid_grant", "error_description": "..." }
```

| Status | `error` |
| --- | --- |
| 400 | `invalid_request`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`, `invalid_redirect_uri`, `invalid_client_metadata` |
| 401 | `invalid_client` |
| 403 | `unauthorized_client`, `access_denied` |
| 500 | `server_error` |

The OAuth endpoints do not return `X-Request-Id`. A 401 `invalid_client` has no `WWW-Authenticate` header.

## OAuth rate limits

The OAuth endpoints have their own limits, counted per IP address in a one-minute window: 5 requests per minute on `/api/v1/oauth/register` and 120 per minute on `/api/v1/oauth/token`. Over the limit you receive 429 with `Retry-After`. Handle any 429 from these endpoints by waiting for `Retry-After`, not by reading the body: the body is not the same in every case. These responses carry no `RateLimit-*` headers. The REST limits are described in [Rate limits](https://docs.filmo.io/rest-api/guides/rate-limits).

## Before you rely on it

Use the OAuth support of your MCP client or a standard OAuth 2.1 library rather than hand-built requests. For server-to-server integrations that act on one account, create an API key instead.

---

Canonical URL: https://docs.filmo.io/rest-api/guides/oauth  
Last updated: 2026-10-09