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.

You are buildingUse
A backend job or integration for your own company that calls the REST API on a schedule or on eventsAn API key, created by an administrator. See Authentication.
An integration that acts for many accounts, or that needs consent the customer can revokeOAuth. 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/mcpOAuth. 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.

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.

StepEndpointNotes
DiscoveryGET /.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.
RegistrationPOST /api/v1/oauth/register (RFC 7591)Open, JSON only. Public client: you receive a client_id and no client_secret.
AuthorizationGET /oauth/authorizePKCE with S256 is mandatory. An administrator of the account sees the consent screen.
TokenPOST /api/v1/oauth/tokengrant_type=authorization_code, or grant_type=refresh_token to rotate.

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.

DocumentFields
/.well-known/oauth-authorization-serverissuer, 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-resourceresource (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.

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
# 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"]}'

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

ParameterRule
client_idRequired. The registered client.
redirect_uriRequired. Must be exactly equal to one of the URIs you registered: no wildcards, and no flexible port for loopback.
response_typeMust be code.
scopeOptional. Space-separated permissions to pre-select on the consent screen. Unknown scopes are ignored.
code_challengeRequired.
code_challenge_methodS256 only; plain is rejected.
stateOptional, 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.

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 and 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.

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
# 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:

{
  "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>"
}
  • The access token expires after 1 hour. After that, calls answer 401 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.

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 or 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.

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. 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.

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.

{ "error": "invalid_grant", "error_description": "..." }
Statuserror
400invalid_request, invalid_grant, unsupported_grant_type, invalid_scope, invalid_redirect_uri, invalid_client_metadata
401invalid_client
403unauthorized_client, access_denied
500server_error

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

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.

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.

  • Guides

    Authentication

    Every operation requires an API key sent as a bearer token. Only an administrator can create a key, and the key acts as that administrator, so decide who owns it before you create it.

  • Access

    Authentication and permissions

    Authenticate with an API key in a header, or let the client sign in with OAuth 2.1. Either way the scopes decide which tools exist.

  • Guides

    Multi-account integrations

    Give each customer account its own credential, store it with that account's timezone and currency, and treat a failing credential as the state of one connection, not of your whole product. OAuth is available for a server you run, and API keys are simpler for server-to-server work.

Was this page helpful?