API Keys
Mint, list, and revoke tenant-scoped API keys — roles, per-key scopes, expiry, and environment constraints — the operational workflow.
API keys are the fallback credential that authenticates calls when you're not presenting an OIDC JWT. Each key is tenant-owned, carries a role/scope bundle, and can be constrained by expiry and environment. This page covers the operational lifecycle API: mint, list, rotate, revoke, and inspect the metadata operators need to run those credentials safely.
Choose the right API-key page
Auth · API keys owns the verification model, storage/hash/prefix scheme, roles and scopes, and threat model. Management · API keys owns mint, list, rotate, revoke, and the operational lifecycle API. Issue & use API keys owns the end-to-end task sequence and validation path.
Where it lives
Keys are served by TenantApiKeyService under /api/v2/api-keys and stored in
the tenant_api_key table. Secrets are bcrypt-hashed at rest — the
plaintext key is returned exactly once, at creation, and is never stored,
logged, or recoverable afterward. The application aggregate is at
application/tenantapikey/service.go.
The key record
ListApiKeys and the create response return an ApiKeyRecord — metadata only,
never the secret or its hash:
Prop
Type
Roles and scopes
roles— validated against Ductor's RBAC catalog. Built-ins areviewer,operator,admin, andservice, plus any roles a module registers. An unknown role is rejected.scopes— optional, fine-grainedresource:actiongrants (e.g.pool:read,rule:write) that are unioned with whatever the roles expand to.
Scope a key to the least privilege its integration needs. A read-only
dashboard key gets viewer; a service that only creates runs gets a narrow
scope set, not admin. For the full verification and scope-expansion model, see
Auth · API keys.
Environment constraints
environment_mode limits which environments a key may mutate:
| Mode | Effect |
|---|---|
all (default) | No environment restriction. |
selected | Only the environments listed in environment_keys. |
production_only | Only production environments. |
non_production_only | Everything except production. |
Supplying environment_keys with an empty mode is treated as selected.
Mint a key
CreateApiKey — POST /api/v2/api-keys — requires tenant:write and the
admin role. The plaintext secret is in the response once.
curl -s -X POST https://api.ductor.io/api/v2/api-keys \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "X-Tenant-ID: $DUCTOR_TENANT" \
-H "Content-Type: application/json" \
-d '{
"name": "routing-service-prod",
"roles": ["operator"],
"scopes": ["pool:read", "recipient:read", "rule:read"],
"environment_mode": "production_only",
"expires_at": "2027-01-01T00:00:00Z"
}'{
"key": {
"id": "b2c3d4e5-...",
"name": "routing-service-prod",
"key_prefix": "duk_9f2a",
"roles": ["operator"],
"scopes": ["pool:read", "recipient:read", "rule:read"],
"status": "active",
"environment_mode": "production_only",
"expires_at": "2027-01-01T00:00:00Z",
"created_at": "2026-07-11T10:35:00Z"
},
"secret": "duk_9f2a1b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a"
}Capture the secret now — there is no second chance
The secret field is output-only and returned exactly once. If you lose it,
you cannot recover it: revoke the key and mint a new one. Store it in a secrets
manager the moment you receive it; never commit it or log it.
Present the secret on subsequent calls as the bearer token:
curl -s https://api.ductor.io/api/pools \
-H "Authorization: Bearer duk_9f2a1b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a" \
-H "X-Tenant-ID: $DUCTOR_TENANT"List keys
ListApiKeys — GET /api/v2/api-keys (tenant:read) — returns the tenant's
keys, active and revoked, newest first. Records never include the secret or
hash.
curl -s https://api.ductor.io/api/v2/api-keys \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "X-Tenant-ID: $DUCTOR_TENANT"{
"keys": [
{ "id": "b2c3d4e5-...", "name": "routing-service-prod", "key_prefix": "duk_9f2a",
"status": "active", "roles": ["operator"], "last_used_at": "2026-07-11T10:40:00Z" }
],
"total_count": 1
}Use key_prefix and last_used_at to reconcile which key is which and spot
stale credentials.
Revoke a key
RevokeApiKey — POST /api/v2/api-keys/{id}/revoke (tenant:write, admin) —
marks the key revoked; it stops authenticating immediately. Revoking an
already-revoked key is a no-op, and revocation cannot be undone.
curl -s -X POST https://api.ductor.io/api/v2/api-keys/b2c3d4e5-.../revoke \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "X-Tenant-ID: $DUCTOR_TENANT"Because revocation is instant and irreversible, rotation is
mint-then-revoke: create the replacement key, cut traffic over to it, confirm
it's serving (watch last_used_at), then revoke the old one. Never revoke
first — you'll create an outage window.
Operational checklist
- Rotate on a schedule using expiry (
expires_at) so keys age out even if no one remembers to rotate them. Expired keys are rejected at the auth path. - One key per integration, named for the integration, so revocation has a blast radius of one consumer.
- Least privilege: prefer explicit
scopesover broad roles; reserveadminfor genuine tenant/key administration. - Audit every lifecycle change — creation, status change, and revocation are
recorded in a hash-chained trail keyed on
tenant_api_key.
Where to go next
Auth · API keys
Verification, storage, prefix lookup, RBAC roles, and scope expansion.
Issue & use API keys
The end-to-end task path for minting, using, validating, and revoking a key.
Tenants
The boundary every key is scoped to.
Managing resources
The conventions — headers, pagination, errors — shared by every call.
Tenants
Provision and manage tenants — the top-level isolation boundary — with quotas, metadata, feature flags, and soft-delete lifecycle.
Pools & Recipients
Manage routing targets — pools (containers with a strategy and kill-switch) and recipients (concrete endpoints with capacity and state) — over the management API.