Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer lb_YOUR_API_KEY
Authentication is enforced in two layers on every protected endpoint:
- Tenant-level feature gate — the caller tenant must have the capability required by the endpoint (e.g.
async_enginefor/v1/jobs,pipelinesfor/v1/pipelines). - API-key level scope gate — since v1.1, the calling API key must carry the matching scope ID. See API Key Scopes (v1.1) below for the full model.
Contents: Required Headers · Standard Response Format · Error Responses · API Key Scopes (v1.1) · Next Steps
Required Headers
| Header | Required | Description |
|---|---|---|
Authorization: Bearer <key> |
Always | API key |
Content-Type |
Varies | application/json, application/pdf, text/plain |
Idempotency-Key |
Create/convert endpoints | Prevents duplicate operations |
Standard Response Format
Successful responses return the resource JSON directly.
Errors always return this structure:
{
"error_code": "ERROR_CODE",
"message": "Human-readable description"
}
Some errors include additional context fields (required_scope, documentation_url, expires_at). They are documented per-code below.
Error Responses
| HTTP | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED |
Missing, malformed, revoked, or unknown API key |
| 401 | APIKEY_EXPIRED |
API key has expires_at in the past |
| 401 | APIKEY_SCOPE_INVALID |
Legacy wildcard key while deny-by-default mode is active |
| 403 | FORBIDDEN |
Tenant suspended, key manually deactivated |
| 403 | INSUFFICIENT_SCOPE |
Key is missing the scope required by the endpoint |
401 Unauthorized — UNAUTHORIZED
Missing or invalid API key:
{
"error_code": "UNAUTHORIZED",
"message": "Missing or invalid API key"
}
Causes: no Authorization header, malformed header, key not found, key revoked.
401 Unauthorized — APIKEY_EXPIRED
The key has an expires_at timestamp in the past. Renew or rotate the key.
{
"error_code": "APIKEY_EXPIRED",
"message": "API key expired at 2026-05-13T12:00:00Z",
"expired_at": "2026-05-13T12:00:00Z"
}
401 Unauthorized — APIKEY_SCOPE_INVALID
Returned when deny-by-default mode is active and the calling key has no explicit scope list (legacy wildcard path). Contact your platform operator to schedule the transition, then re-issue the key with an explicit scope list.
{
"error_code": "APIKEY_SCOPE_INVALID",
"message": "API key has no scopes assigned. Re-issue the key with an explicit scope list.",
"documentation_url": "https://docs.zplflow.io/docs/authentication#api-key-scopes-v11"
}
403 Forbidden — FORBIDDEN
Tenant or key admin-state rejection:
{
"error_code": "FORBIDDEN",
"message": "API key is inactive"
}
Causes: key manually deactivated, account suspended, tenant missing required feature.
403 Forbidden — INSUFFICIENT_SCOPE
The calling key is valid and the tenant has the required feature, but the key does not carry the scope required by the endpoint.
{
"error_code": "INSUFFICIENT_SCOPE",
"message": "API key is missing required scope 'jobs:create'",
"required_scope": "jobs:create",
"request_id": "f7c8e0a1-2b34-4c5d-9e8f-0123456789ab",
"documentation_url": "https://docs.zplflow.io/docs/authentication#api-key-scopes-v11"
}
Use required_scope to identify which grant to add to the calling key via POST /v1/apikeys (replace) or by editing existing keys. Never echo or log the raw key value.
API Key Scopes (v1.1)
A scope is a fine-grained permission grant attached to a single API key. Scopes complement, they do not replace, the existing tenant-level plan and feature model. Two keys belonging to the same tenant can have completely different scope sets.
Why scopes
- Least privilege per consumer. Issue a partner a read-only key while the WMS keeps the master key.
- Distinct auditable identities. A CI bot and a human user can be told apart in logs and rate limits without sharing secrets.
- Safer onboarding. A new service gets only the endpoints it needs; revocation is per-key, never tenant-wide.
- MCP parity. MCP tools enforce the same scope IDs as the REST API — no separate permission model.
The three permission states
Each API key has an effective permission state that determines what it can do. There are three meaningful states:
| State | Semantics | Behavior |
|---|---|---|
| Wildcard legacy | Pre-deny-by-default path. Treated as the universal scope set. Once deny-by-default mode is active, this state is invalid (see APIKEY_SCOPE_INVALID). |
Allowed only while deny-by-default mode is not active. |
| Locked (no scope grants) | Deny-by-default. Only endpoints with no required scope, or scopes marked public in the catalog, are reachable. All other endpoints return 403 INSUFFICIENT_SCOPE. |
Reach only catalog entries marked public. |
| Scoped (explicit scope grants) | Enforce. Each request requires the scope listed in the catalog for that endpoint. | Reach only endpoints whose required scope appears in the key’s grant list. |
A key with no scope grants issued (e.g. created on the legacy wildcard path or with an empty scope list) sits in one of the wildcard legacy or locked states depending on the platform mode.
Catalog of scopes
The full set of supported scopes is published at GET /v1/scopes (no authentication required). Every scope entry carries:
- an
id(the string passed in the key’s scope grant list) - a
group(convert,jobs,pipelines,account,audit,gdpr,admin) - the HTTP endpoint(s) and MCP tool(s) it unlocks
- the tenant feature required for the endpoint to be meaningful
- a
public: boolflag (endpoints marked true are reachable by locked keys too)
The catalog is also exposed at runtime:
- HTTP —
GET /v1/scopes(no auth, returns the full catalog described below) - CLI —
zplflow scopes listandzplflow scopes show <scope_id>(see CLI Tool)
Inline reference, by endpoint, is documented in Conversions, Jobs API, Pipelines, and Self-Service API Keys.
Activation flow
If your account was created before deny-by-default mode was enabled, your existing keys operate in wildcard legacy mode. The platform operator runs a one-time transition that converts all such keys to locked; afterward every key must be re-issued with an explicit scope list via POST /v1/apikeys or the admin UI “Generate Key” modal.
Once deny-by-default mode is active, wildcard legacy keys receive 401 APIKEY_SCOPE_INVALID (fail-loud, not silent deny). The procedure is reversible on request until each operator key has been re-issued.
The apikeys:manage scope
To administer keys via the self-service endpoints (GET /v1/apikeys, POST /v1/apikeys, DELETE /v1/apikeys/{id}) the calling key must carry apikeys:manage. Sessions are tenant-scoped — you can only list, create, and revoke keys belonging to the tenant identified by the bearer token. See Self-Service API Keys for the full reference.
Expiration and last-used
Each API-key record may carry an optional expires_at (UTC, RFC3339). Past-expiry keys return 401 APIKEY_EXPIRED. last_used_at is updated asynchronously by the auth middleware, throttled to one write per five minutes per key (no hot-write).
Next Steps
- Self-Service API Keys — manage keys programmatically
- Conversions — Sync conversion endpoints
- Jobs API — Async job workflow
- Pipelines — Pipeline scope map
- Best Practices — Least-privilege recipes