Authentication
Every QuantumBPM API endpoint requires a bearer token in the Authorization header. The platform doesn't ship its own identity store — tokens are issued by an OpenID Connect (OIDC) provider, and the backend validates them on every request.
This page is the integrator's reference: how to get a token, how to use it, and what role/permission model is applied to your calls.
How auth works
┌──────────────┐ client credentials ┌─────────────────┐
│ Client │ ──────────────────────► │ OIDC provider │
│ (worker / │ │ (Zitadel / │
│ SDK / app) │ ◄────── access token ── │ your IdP) │
└──────────────┘ └─────────────────┘
│
│ Authorization: Bearer <token>
▼
┌──────────────────────────────────────┐
│ Platform API │
│ • Validates the token's signature │
│ • Reads identity, roles, projects │
│ from token claims │
│ • Applies project RBAC │
└──────────────────────────────────────┘
The token's claims tell the platform who the caller is, which projects they can access, and at what role. The backend never makes its own decision about permissions — it trusts what your IdP signs.
SaaS
The managed SaaS uses Zitadel as its identity provider. Manage users, service accounts, and identity providers from the Auth Console (auth.quantumbpm.com).
Two kinds of tokens:
| Use case | How to obtain |
|---|---|
| Interactive user | Sign in to the Platform Console; the SPA handles the OIDC flow for you. The token in localStorage works for direct API calls, though it's tied to a browser session and expires |
| Service account (workers, CI, integrations) | Create one in the Auth Console: Service Accounts → New. You'll get a client ID + client secret pair. Use the OAuth 2.0 client credentials grant to mint access tokens |
Service accounts are the right answer for anything unattended — workers, scheduled jobs, your backend calling decisions or starting processes. They don't expire on a user session and the role assigned to the service account in the Auth Console maps directly to platform permissions.
Enterprise / self-hosted
Enterprise builds validate against any standards-compliant OIDC provider — Keycloak, Okta, Auth0, Azure AD, Zitadel, etc. The platform doesn't run its own identity infrastructure; it consumes whatever you already use.
The deployment-time operator configures the issuer URL and the claim paths the platform reads (roles claim, projects claim). For end users — including SDK and worker authors — the contract is the same as SaaS: get a token from your IdP, send it as Authorization: Bearer <token>.
If you're standing up a self-hosted instance, see the Enterprise deployment guide for the configuration options. If you're just using an Enterprise deployment, ask your operator for:
- The issuer URL and token endpoint of your IdP.
- A client ID / secret for service-account use, or instructions for getting a personal token.
Getting a token
For a service account using the OAuth 2.0 client credentials grant:
curl -X POST "$ISSUER/oauth/v2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=openid profile"
The response carries an access_token and an expires_in (in seconds). Cache the token until shortly before it expires, then mint a new one. Don't request a fresh token on every API call — your IdP will rate-limit you and your latency will suffer.
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600
}
The exact token endpoint URL varies by provider. Zitadel uses /oauth/v2/token; Keycloak uses /realms/{realm}/protocol/openid-connect/token; consult your IdP's documentation.
Using the token
Send the token in the Authorization header on every API call:
curl "$API/projects/$PROJECT/bpmn/instances" \
-H "Authorization: Bearer $TOKEN"
A few practical points:
- Refresh before expiry, not after. If you wait until you get a
401, in-flight calls fail. Refresh when the token has, say, a minute left. - Tokens are bearer credentials. Anyone who has the token can act as you until it expires. Don't log them, don't commit them, and rotate the underlying client secret regularly.
- One token per service account is fine. Multiple workers can share the same token — the IdP doesn't care, and the platform only reads claims from it.
The role model
The platform applies access checks at two levels.
Organisation-level roles
Set on the user / service account in your IdP. Surface as global roles:
| Role | Granted by | Effect |
|---|---|---|
user | Default | Can sign in and access projects they're explicitly members of |
admin | Org admin | Manage the organisation; not the same as project admin |
system-admin | Operator only | Implicit access to every project. Reserved for platform operators |
Project-level roles
Set per project, per member. Strict hierarchy — each role implicitly carries everything below it:
admin ⊃ editor ⊃ executor ⊃ viewer
| Role | Can do |
|---|---|
viewer | Read decisions and processes; view executions and instances |
executor | Everything viewer can do, plus: call decisions via API, start instances, complete user tasks they're assigned, complete external jobs |
editor | Everything executor can do, plus: create and modify decisions, processes, and resources |
admin | Everything editor can do, plus: manage project members and settings |
For SaaS, project membership and roles are managed from the Platform Console (Project Settings → Members). For Enterprise, project memberships come from token claims — see the Enterprise deployment docs.
Where the role applies
Every endpoint requires a specific minimum project role. The most common shapes:
- Read endpoints (list / get instances, jobs, user tasks, definitions) —
viewerfor most,executorfor/user-tasksto keep the surface tight. - Action endpoints (start instance, send message/signal, complete user task, complete external job) —
executor. With one exception: a user task's ownassignee/candidateUsers/candidateGroupsmember can complete or fail it withoutexecutor— the assignment counts as authorisation. See User tasks → Access control. - Authoring endpoints (create / update / delete / deploy resources) —
editor. - Project administration (members, settings) —
admin.
Why a deny looks like a 404
When the platform refuses to surface a project to a caller — either because the caller doesn't have access at all, or because their role is too low for a specific action — it returns 404 Not Found rather than 403 Forbidden.
This is intentional: it avoids confirming whether a project ID exists at all. If you're getting a 404 you didn't expect, the most common causes are:
- The caller's token has no membership claim for that project.
- The caller's role on the project is below what the endpoint requires.
- The project ID is genuinely wrong.
Check your token's claims (decode it at jwt.io for a quick look) — the project list and roles will be in there.
Token expiry and clock skew
Two failure modes that usually trip up first-time integrators:
| Symptom | Likely cause |
|---|---|
401 Unauthorized on a previously-working token | Token expired. Mint a new one |
401 Unauthorized on a freshly-minted token | Server clock is ahead of your IdP's. Tokens carry an iat (issued-at) claim that the validator rejects when it's slightly in the future. Sync time on both ends, or adjust the IdP's clock-skew tolerance |
Sporadic 401s under load | Multiple workers minting and immediately invalidating each others' tokens — make sure each service account uses its own token, cached locally |
Where it shows up next
- BPMN external workers — workers authenticate with the bearer token to poll for jobs.
- BPMN user tasks — your inbox UI uses the token to list and complete tasks for the signed-in user.
- Definitions and deployment — uploading and deploying processes is gated on
editorrole.