API keys and sign-in

Give your backend an API key scoped to one project or the whole organization, in live or sandbox mode, and revoke it the moment you need to.

3 min readEdit this page
On this page

Every call under /v1 carries a credential in its Authorization header. Your own code uses an API key, sent as a bearer token:

http
Authorization: Bearer flw_ak_live_…

People use their sign-in. The console and the Try it button on reference pages sign you in as yourself at auth.flowion.io, which issues a short-lived access token bound to a key that only your browser holds. It is sent with a fresh proof of that key on every request (DPoP):

http
Authorization: DPoP <access token>
DPoP: <proof for this method and URL>

A copied access token is useless without that key, so it cannot be pasted into a script: anything that runs unattended uses an API key. The API tells the two apart by the header, so there is nothing to configure.

Make an API key

The quickest way is the console: Settings → API keys → Make a key. You choose three things:

ChoiceOptions
Modelive reaches real chargers and real sessions. sandbox reaches test data and virtual chargers only, and is never billed
ReachOne project, or every project
ExpiryA date, or none. A key without one works until you revoke it

Make one key per system that calls the API, so you can revoke any one of them without touching the others.

You can also make keys through the API, with the identity.apikey.manage permission:

POST/v1/organizations/$ORG/api-keysbash
curl -X POST "$API/organizations/$ORG/api-keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "billing sync",
       "policyBindings": [
         {"policyId": "'"$USER_POLICY"'", "projectId": "'"$PROJECT"'"}
       ],
       "mode": "live"}'
200Response
{
  "id": "0b1c7f3e-2a4d-4b6e-9c8f-1d2e3f4a5b6c",
  "organizationId": "b3f5a1c2-4d6e-4f80-9a1b-2c3d4e5f6a7b",
  "projectId": "9a7c0e12-3b4d-4e5f-8a6b-7c8d9e0f1a2b",
  "name": "billing sync",
  "keyId": "a1b2c3d4",
  "policyBindings": [
    {
      "policy": {
        "id": "00000129-0000-4000-8000-000000000003",
        "name": "User"
      },
      "projectId": "9a7c0e12-3b4d-4e5f-8a6b-7c8d9e0f1a2b"
    }
  ],
  "mode": "live",
  "createdBy": "user-7f3a",
  "createdAt": "2026-10-01T10:00:00Z",
  "expiresAt": null,
  "lastUsedAt": null,
  "revokedAt": null,
  "secret": "flw_ak_live_a1b2c3d4_..."
}

keyId is the public part of the key, safe to show in logs and screens.

Choose the key's reach

A key holds exactly one binding in policyBindings. Get the policy ids from List permission policies.

BindingReaches
The User policy at one projectExactly that project, named by projectId. Its chargers, sessions and everything else inside it, and nothing outside
The Admin policy at the organization (no projectId)Every project in the organization, now and later, plus members and API keys

Give each system the narrowest key that does its job. A backend that only works with one location's chargers wants User at that project; a tool that manages the whole organization wants Admin. A key can never hold Owner, so no unattended system can delete your organization or hand it to someone else. Permissions and policies has the full picture.

Anything but exactly one such binding answers 422 invalidScope, as does naming a project from another organization or setting expiresAt in the past.

Choose live or sandbox

A key works in one mode, written into the start of its secret: flw_ak_live_… or flw_ak_test_… (the mode field reads sandbox for the second). A key reaches only its own mode's data, so a sandbox key can never touch a real charger. Leave mode out and you get a live key.

Retry a key request safely

Add an Idempotency-Key header if your tooling might send the request twice. A repeat with the same key and body makes nothing new and answers 409 idempotencyKeyReplayed, naming the key already made. Since its secret cannot be shown again, revoke that one and make another if the first response was lost.

List and revoke keys

bash
curl "$API/organizations/$ORG/api-keys" \
  -H "Authorization: Bearer $TOKEN"

curl -X DELETE "$API/organizations/$ORG/api-keys/$KEY_ID" \
  -H "Authorization: Bearer $TOKEN"

The list shows every key the organization has made, revoked and expired ones included, newest first, and never a secret. lastUsedAt tells you whether a key is still in use before you revoke it.

Revoking takes effect on the very next request. Revoking a key twice is fine: it answers 204 both times. The revoked key stays in the list with revokedAt set, so you keep the record of what could reach your API, and when.

When a token is refused

A token that cannot be used answers 401 with the standard error body:

json
{
  "error": "invalidToken",
  "message": "...",
  "requestId": "7f1c0e2a-5b8d-4c1e-9a53-2f0d6f4b1e77"
}

invalidToken covers an expired, revoked or unknown token alike. missingOrMalformedHeader means the Authorization header is absent or is neither Bearer nor DPoP.

A token proves who is calling. What it may reach is checked against the organization's current members and keys on every request, so a change in the console applies at once. If that check cannot be made, the request is refused rather than let through.

Next: Your first API call.

In the API reference