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:

  1. Tenant-level feature gate — the caller tenant must have the capability required by the endpoint (e.g. async_engine for /v1/jobs, pipelines for /v1/pipelines).
  2. 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: bool flag (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 list and zplflow 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