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
Link to this section: 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:// | 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.
How the flow works
Link to this section: How the flow worksThe 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 /. (RFC 8414) and GET /. (RFC 9728) | Both are public. The 401 from /api carries a WWW-Authenticate header with resource_metadata, so a client finds them without configuration. |
| Registration | POST /api (RFC 7591) | Open, JSON only. Public client: you receive a client_id and no client_secret. |
| Authorization | GET /oauth | PKCE with S256 is mandatory. An administrator of the account sees the consent screen. |
| Token | POST /api | grant_type=authorization_code, or grant_type=refresh_token to rotate. |
Discovery
Link to this section: DiscoveryRead the metadata instead of hard-coding endpoints. Both documents are served under /. and can be cached for five minutes. In production the issuer, the endpoints and the protected resource are always on https://: a request to /api or to a . document on any other production host is answered with a 308 redirect to it.
| Document | Fields |
|---|---|
/. | issuer, authorization_endpoint (/oauth), token_endpoint (/api), registration_endpoint (/api), 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 |
/. | resource (the MCP endpoint, https://), 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
Link to this section: Register a clientRegistration 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_urisis required, with at most 10 entries. Each one is an absolute URI without a fragment, and is eitherhttps, orhttpon a loopback host such as127.or0. 0. 1 [::1](any port and path), or a custom scheme of a native app.client_nameis 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 withtoken_endpoint_auth_methodset tonone.- The answer is 201 with
client_id,client_id_issued_at,client_name,redirect_uris,grant_types,response_types,token_endpoint_auth_methodandtoken_endpoint. - A client that is never used may be purged after 30 days, so register once and keep the
client_id.
# 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
Link to this section: AuthorizeSend the administrator's browser to /oauth on https://. 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.
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.
Get and refresh tokens
Link to this section: Get and refresh tokensPost to /api as a form (application) or as JSON. The client is public: send client_id, and no secret.
grant_type=authorization_coderequiresredirect_uri,codeandcode_verifier.grant_type=refresh_tokenrequiresrefresh_token.- Any other grant type is rejected with
unsupported_grant_type. There is noclient_credentials. scopeandresourceon the token request are ignored: the scopes are the ones granted on the consent screen.
# 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>"
}Lifetime and rotation
Link to this section: Lifetime and rotation- The access token expires after 1 hour. After that, calls answer 401
api_key_expireduntil 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
Link to this section: Use the tokenSend 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.
Revoke a connection
Link to this section: Revoke a connectionThere 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.
OAuth errors
Link to this section: OAuth errorsThe OAuth endpoints do not use the envelope of the rest of the API, and they do not answer in application. 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": "..." }| 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
Link to this section: OAuth rate limitsThe OAuth endpoints have their own limits, counted per IP address in a one-minute window: 5 requests per minute on /api and 120 per minute on /api. 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.
Before you rely on it
Link to this section: Before you rely on itUse 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.
Related pages
Link to this section: Related pagesGuides
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?