Authentication
Every endpoint under /v1 — everything except /livez and /readyz —
expects a bearer token:
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:
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:
{
"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.
List and revoke round out the surface:
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/identitycannot 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
emailclaim, it is attached to the caller's identity only whenemail_verifiedistrue. 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:
{ "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.