GuidesAPI reference
Introduction
  • Overview
Orientation
  • What a CSMS does
  • OCPP in ten minutes
  • The vocabulary
  • Lifecycle of a charging session
  • Which OCPP version am I dealing with?
  • How Flowion maps onto OCPP
Getting started
  • Quickstart
  • Core concepts
  • Set up your workspace
  • Connect a simulated charger
  • Connect a real charger
  • Environments
API guide
  • Authentication
  • Your first API call
  • Interactive API reference
  • Roles and permissions
  • Errors, pagination and versioning
  • Live updates
  • Sending commands to a charger
  • Rate limits
How-to guides
  • Register and provision a charger
  • Read and change charger settings
  • Authorize drivers with RFID cards
  • Start and stop sessions remotely
  • Track sessions and energy
  • Take a charger in and out of service
  • Unlock a stuck connector
  • Reset a charger
  • Send vendor-specific data
  • Keep a site within its electrical limits
  • Use day-ahead electricity prices
  • Receive events with webhooks
  • OCPP feature coverage
Troubleshooting
  • 8 more coming soon
    • My charger will not connectSoon
    • It connects, then dropsSoon
    • A command timed out or returned 409Soon
    • A card is not being authorizedSoon
    • My transaction looks wrongSoon
    • Meter values are missing or sparseSoon
    • Reading the OCPP message logSoon
    • Known limitationsSoon
Reference
  • 5 more coming soon
    • GlossarySoon
    • Endpoint indexSoon
    • OCPP message supportSoon
    • Status and error codesSoon
    • ChangelogSoon
Flowion Docsflowion.io
PreviousEnvironmentsNextYour first API call
Was this page helpful?
Edit this page

Authentication

Every endpoint under /v1 — everything except /livez and /readyz — expects a bearer token:

HTTP
Authorization: Bearer <token>

Two credential shapes are accepted, and the API tells them apart by looking at the string itself: an Identity Platform-issued JWT (an interactive human login through the console), or a Flowion-issued API key, which always starts with flw_live_. There is nothing else to configure to pick one over the other — present whichever you have.

API keys#

An API key is a durable credential your own backend can hold, minted through the API itself rather than a console session — the credential this page used to say did not exist yet.

A key reaches exactly one environment, no matter its role. This is the single most important thing to get right: an environment is not a convenience default the way it is for a human Owner or Admin, who reach every environment in their organization implicitly. A key never gets that implicit reach, at any role — an owner-role key minted for staging gets nothing in prod, not even read access, even though a human Owner in that same organization reaches both. If you need a backend to talk to two environments, mint two keys.

Minting requires Owner or Admin, and a key's role can never exceed the minting member's own role, enforced server-side rather than trusted from the request:

Shell
curl -X POST "$API/environments/$ENV/api-keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "billing sync", "role": "user", "expires_at": null}'

role is owner, admin, or user — the same three roles a human membership carries. An Admin requesting an owner-scoped key gets 403, never a silently downgraded key. expires_at is optional; omitted, the key never expires on its own (it stays revocable at any time regardless).

The response carries the secret, once:

JSON
{
  "id": "0b1c...",
  "environmentId": "9a7c...",
  "organizationId": "b3f5...",
  "name": "billing sync",
  "prefix": "a1b2c3d4",
  "role": "user",
  "createdAt": "2026-08-31T10:00:00Z",
  "expiresAt": null,
  "lastUsedAt": null,
  "revokedAt": null,
  "key": "flw_live_a1b2c3d4e5f6..."
}

key is returned exactly once, on this response, and never again. This API stores only its SHA-256 digest — there is no reveal endpoint, and there will not be one, for the same structural reason a webhook signing secret is never re-shown: nothing downstream of the create call, not this API and not a database dump, can produce the value a second time. Losing it means revoking that key and minting a replacement.

Field casing is not uniform here

The rest of this API is snake_case throughout, and CreateApiKeyRequest's own request body follows that (expires_at). The key resource this API hands back — ApiKeyView, in every response above — is camelCase (createdAt, environmentId, expiresAt, lastUsedAt, revokedAt). That is a real inconsistency in the shipped shape, not a typo in this page: match the casing shown above exactly, per field, rather than assuming the rest of the API's convention holds here too.

List and revoke round out the surface:

Shell
curl "$API/environments/$ENV/api-keys" \
  -H "Authorization: Bearer $TOKEN"

curl -X POST "$API/environments/$ENV/api-keys/$KEY_ID/revoke" \
  -H "Authorization: Bearer $TOKEN"

Listing never carries a secret — only what create's response carried besides key. Revoking is permanent and idempotent: there is no delete, so a revoked key's row survives with revokedAt set, and revoking it again just reports the same original timestamp rather than erroring. The row staying around is deliberate — it is exactly the audit trail an Owner or Admin needs when asking "what could reach our API, and when."

lastUsedAt updates only every five minutes or so at most, even under constant traffic — it is throttled server-side so that recording use never adds a write to every single request. Treat it as an audit signal ("has this key been used recently, at all") rather than a liveness check ("is a request happening right now").

A missing, unrecognized, expired, or revoked key gets the same 401 a bad JWT gets, with the same shared error body — see Errors, pagination and versioning.

The console session, for interactive use#

If you are signed in to the console yourself and want to explore the API interactively rather than mint a key, the same session carries a JWT you can read from /api/auth/session's idToken. That token is short-lived and tied to the browser session — reasonable for a curl you are typing by hand while reading these docs, not something a backend should depend on. Minting an API key is what that backend should hold instead. See Set up your workspace for the same point in context.

What either credential proves, and what it does not#

A verified credential — JWT or key — authenticates a caller. It does not, by itself, name an organization or a role; those are never trusted from the credential and are resolved fresh against real membership (a human's, or the key's own row) on every request:

  • 404, not 403, for a non-member. If the caller cannot access the organization or environment a resource belongs to, the API answers as if the resource did not exist — there is nothing to distinguish "you can't see this" from "this doesn't exist." A caller with some access but an insufficient role instead gets 403, because they already know the resource exists. See Roles and permissions for the full breakdown, including how an API key's environment-only reach changes this.
  • Fails closed. If services/identity cannot be reached to answer the membership or key check, the request is refused rather than allowed. An outage in the authorization path is never treated as "permit."
  • Verified email only, for a human. If a JWT carries an email claim, it is attached to the caller's identity only when email_verified is true. An API key carries no email at all.

Token and key verification#

A JWT is verified against the deployment's own Identity Platform project — issuer and JWKS endpoint are derived from that project id, not a hardcoded vendor assumption. An API key is verified by its flw_live_ prefix (a JWT's base64url header always starts eyJ, so the two are never ambiguous) and its SHA-256 digest looked up directly; an unknown key and a wrong key resolve identically, so there is no timing signal that distinguishes "this key never existed" from "this key exists but doesn't match." Either failure mode — an unverifiable JWT or an unrecognized key — gets 401 with the shared error body:

JSON
{ "error": "invalid_token", "message": "..." }

See Errors, pagination and versioning for the shape every error response shares.

Scoping is per-resource, not carried by the credential#

Nothing about a credential itself is organization- or environment-scoped in the sense of being asserted by the client. A human's JWT can belong to a user who is a member of several organizations; which one a given request concerns is decided entirely by the resource being addressed — the organization or environment id in the path — checked against real membership or, for a key, its own stored environment_id. There is no "current organization" header, and a client cannot switch organizations by sending something different, only by which resource it asks for.

Next: Your first API call.