Skip to content
Docs / Authentication

Authentication

Flik uses OAuth 2.0 client credentials. You exchange a key pair for a short-lived bearer token, then send that token on every API call.

API keys

Create keys under API Keys in the Flik portal. Each key is a pair:

clientId string
Identifies the key. Not secret in the sense that a password is, but there is no reason to publish it either.
clientSecret string

Proves the request is yours. Shown once, at creation. Flik stores only a hash and cannot recover it — if you lose it, create a new key.

The secret is server-side only Anyone holding it can create payments that settle to your account. Keep it out of browser code, mobile apps, and version control. Load it from an environment variable or a secret manager.

Test and live keys

A key is test or live from the moment it is made, and the portal decides which by its own Test Mode switch, at the bottom of the sidebar. Turn it on before you create the key and you get a test key; leave it off and you get a live key. The mode is visible in the credential itself:

Prefix Mode Effect
flik_live_ Live Real payments. Money moves.
flik_test_ Test Sessions behave normally, no money moves, no bank is contacted. Responses carry "testMode": true.

The prefix exists so mistakes are visible. Assert on it when your application boots — a staging environment holding a flik_live_ key should refuse to start rather than take a real payment during a test run.

There is no separate sandbox host. Test and live use the same base URL and the same endpoints; the key decides. See Testing.

Getting an access token

post /api/token

Send your key pair as application/x-www-form-urlencoded. This is the one Flik endpoint that does not take JSON — post it as a form body and a JSON body is rejected as missing credentials, which reads confusingly if you are not expecting it.

This endpoint takes no Authorization header. The key pair in the body is the credential. Every other endpoint is the other way round: a bearer token in the header and no credentials in the body.

Not HTTP Basic Flik does not accept the key pair as a Basic Authorization header, and does not accept client_id/client_secret in snake_case. The field names are clientId and clientSecret exactly.

Parameters

clientId string Required
The client ID from your API key.
clientSecret string Required
The matching client secret.

Response

200 OK
{
  "accessToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": "1h"
}
accessToken string
The bearer token. Treat it as opaque — its internal structure is not part of this contract and may change.
tokenType string
Always Bearer.
expiresIn string
How long the token is valid for, as a duration string. Currently 1h. Read this rather than assuming it.

Using the token

Send it as a bearer token on every other API call:

Request header
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...

What the token grants

A token carries the organisation the key belongs to and nothing else. There are no scopes to request and no per-endpoint permissions: a token can do everything the merchant API offers, for that one organisation, and nothing at all for any other. A request naming another organisation's session or payment link comes back 404 rather than 403, because a 403 would confirm the id exists.

Test and live are decided by the key, not by the token and not by the URL. A token minted from a flik_test_ key can only ever create test payments.

Token lifetime

Tokens are valid for one hour. Long enough that a token exchange is a once-an-hour cost rather than something on the path of every call, short enough that one found in a log afterwards has almost certainly died with the hour. Your API key is the standing credential; the token is not.

Read the lifetime from expiresIn rather than hard-coding an hour. It has already changed once — it was five minutes — and a client that read the field kept working while one that assumed the number did not.

Two failure modes worth avoiding:

  • Caching a token past its life. You get 401 token_expired at an unpredictable moment, usually under load, because that is when the cached value is being reused hardest.
  • Fetching a fresh token for every request. It works, but it doubles your call count and puts a password hash comparison in front of every operation.

The sensible pattern is to cache the token in memory, expire it slightly early so a request never starts with a token that dies mid-flight, and refresh on demand. The panel alongside shows it. Cache in memory, not in a shared store: a token is cheap to re-mint and a shared cache is one more place it can leak from.

Authentication errors

Status Code Meaning
401 unauthorized No Authorization header, or it is not a Bearer token.
401 token_expired The token was valid but has passed its expiry. Fetch a new one and retry the request once.
401 invalid_token The token could not be verified. Usually a truncated or mangled header.
401 unknown_api_key The token is well-formed but its key no longer exists — it was removed after the token was issued.

token_expired is the only one worth retrying automatically, and only once. See Errors for the full catalogue.

Errors from the token endpoint

This endpoint predates the error envelope: error is a plain string here, so branch on the status.

Status Cause Fix
400 clientId or clientSecret missing, empty, or sent more than once. Usually a JSON body where a form body was expected, or snake_case field names.
401 The credentials did not match. Unknown client ID and wrong secret give the same response. Check for a truncated secret, a stray newline from a copy, or a test key against a live integration. If you no longer have the secret, create a new key.
500 Flik-side failure. Retry with backoff; tell us if it persists.
Why one message for two causes Distinguishing "no such client" from "wrong secret" would let anyone with a list of guesses learn which client IDs are real. The single 401 is the point, not an oversight.

Rotating a key

Keys have no expiry. To rotate: create a second key in the portal, deploy it, confirm traffic has moved, then ask us to revoke the old one — keys cannot yet be deleted from the portal. Both work simultaneously, so there is no cutover window.