Authentication

Last updated 16 August 2026

An apikey header beside an OAuth bearer token, with what each one is suited to

Every API request acts as a user. Whatever that person can see and change, the request can see and change, and nothing more. Their name is what appears in the history of anything the request touches.

There are two ways to prove who you are.

The two credentials

API key OAuth
Looks like apikey: tCgjfl… Authorization: Bearer …
One per user user / integration
Expires never depends on the type
Revoke one integration not possible, one key serves them all yes, independently
Stored by Flexie as the key itself a fingerprint of the token
Best for a quick script, a first integration anything you intend to keep

Both are accepted on every endpoint. You do not have to choose up front, a workspace can start with the key and move to OAuth later, per user, without a flag day.

Using the API key

Send it in the apikey header:

curl "https://your-subdomain.flexie.io/api/leads?limit=5" \
  -H "apikey: YOUR_API_KEY"

Or as a query parameter, which is handy for a quick test in a browser:

curl "https://your-subdomain.flexie.io/api/leads?limit=5&apikey=YOUR_API_KEY"

The header name is apikey, lower case, one word. X-Api-Key, Api-Key and ApiKey are not recognised.

Where to find it: open the user in Settings → Users, then the API Settings tab. The key is shown there and can be regenerated. Regenerating takes effect once you click Update: and it breaks every integration using the old key at that moment, because a user has exactly one key.

Do not send an Authorization header with an API key. A request carrying Authorization: Bearer … is treated as an OAuth request, and your key is not consulted. Send one credential or the other, never both.

Using OAuth

OAuth issues a separate credential per integration, so you can switch one off without touching the others, and you can see which integration did what.

For a server-side integration

This is what most integrations want: no browser, no login screen, one long-lived token.

  1. Open the user the integration should act as, in Settings → Users
  2. Go to the API Settings tab
  3. Click Add API client, name it after the integration, and save
  4. Copy the token, it is shown once

Then send it as a bearer token:

curl "https://your-subdomain.flexie.io/api/leads?limit=5" \
  -H "Authorization: Bearer YOUR_TOKEN"

Everything else about the request is identical. Same paths, same bodies, same responses.

For an app used by many people

An app that acts on behalf of whoever signs in uses the standard authorization-code flow against your workspace's OAuth server. It must request the api scope by name:

GET https://your-subdomain.flexie.io/oauth2/authorize
      ?response_type=code
      &client_id=YOUR_CLIENT_ID
      &redirect_uri=https://yourapp.example.com/callback
      &scope=api
      &state=RANDOM
      &code_challenge=…&code_challenge_method=S256

PKCE with S256 is required. Exchange the code at POST /oauth2/token as usual.

A client that asks for no scope gets mcp only, access to Flexie's AI tools, not the REST API. The api scope is never granted by omission.

Discovery

The REST API publishes its own RFC 9728 metadata:

curl "https://your-subdomain.flexie.io/.well-known/oauth-protected-resource/api"
{ "resource": "https://your-subdomain.flexie.io/api",
  "authorization_servers": ["https://your-subdomain.flexie.io"],
  "scopes_supported": ["api"],
  "bearer_methods_supported": ["header"] }

The document at the bare /.well-known/oauth-protected-resource path describes the MCP server instead. Follow the one the WWW-Authenticate header points you at.

Moving a user from the key to OAuth

The two credentials are deliberately exclusive per user, so there is never a moment where both work and you cannot tell which an integration is using.

As soon as a user has one active API client, their API key stops working. Their next key-authenticated request gets:

{ "error": "api_key_disabled",
  "error_description": "This API key is disabled because an active OAuth client with the api scope exists for this user. Authenticate with OAuth instead, or deactivate that client to re-enable the key." }

Going back is one click. Switch the client off, or delete it, on that user's API Settings tab, and the key works again on the very next request. Nothing is lost, and no other user is affected: the switch is per person, and it is triggered by that person's own clients.

A sensible migration therefore looks like: create the client, move your integration over, confirm it works. If anything goes wrong, switch the client off and your old key is live again while you investigate.

Permission to use the API at all

Separately from which credential you hold, a user's role decides whether they may use the REST API. In Settings → Roles, under API Permissions, tick Access REST API.

A user whose role does not grant it is refused whichever credential they present:

{ "error": "access_denied",
  "error_description": "This account does not have REST API access. Ask an administrator to enable \"Access REST API\" for its role." }

That refusal is a 403, not a 401, because presenting a different credential will not help. Administrators always pass.

Every refusal, and what to do

Status error What happened What to do
401 invalid_grant The API key is missing or unknown Check the apikey header spelling and the key itself
401 api_key_disabled The key is valid, but this user has an active API client Use OAuth, or switch that client off
401 invalid_token The bearer token is unknown, expired, revoked, from an inactive client, or issued for a different resource Get a fresh token; if it was issued for MCP it will never work here
403 access_denied The user's role does not grant Access REST API Enable it on the role
404 none The request never reached the API See below

The OAuth refusals carry a WWW-Authenticate header pointing at the discovery document, so a standards-aware client can restart the flow on its own:

WWW-Authenticate: Bearer resource_metadata="https://your-subdomain.flexie.io/.well-known/oauth-protected-resource/api", error="invalid_token"

A 404 on a path you know exists usually means no credential was sent at all. Requests to /api arrive through a proxy that expects either an apikey or an Authorization: Bearer header, and turns anything else away before Flexie sees it. If a call returns 404 and an HTML page rather than JSON, check that your credential is actually being sent, some HTTP clients drop custom headers on redirect.

A token is bound to one resource

Flexie's OAuth server protects two things: the REST API (api) and the MCP server for AI agents (mcp). A client may hold both scopes. A token may not. Every token is bound to one of the two resources, and the other refuses it.

That is the part worth understanding, because it is what stops a token you hand to an AI agent being turned around and used against your records through the REST API.

So a client authorised for both asks for a token per resource, naming which one it wants with the resource parameter (RFC 8707):

POST /oauth2/token
  ...&scope=api mcp&resource=api      -> a token the REST API accepts
  ...&scope=api mcp&resource=mcp      -> a token the MCP server accepts

Ask for both scopes without naming a resource and you get a token neither will take. There is no sensible way to pick for you, so the server binds it to nothing rather than guessing, and both resource servers reject it. If a freshly issued token is refused everywhere, this is why: add resource.

A value the server does not recognise, or one naming a scope you were not granted, is ignored the same way, so resource=https://example.com also ends in a token bound to nothing.

A Service Token client is the exception, and it is enforced. Its token is minted the moment you save, with no request to carry a resource, so the form requires exactly one scope. Tick both and it refuses to save. If you want machine-to-machine access to both, create one Service Token client per resource.