Tenant API keys.
Let the members of your customers' tenants create API keys for their own automation, scoped to one tenant and to scopes your app defines. Auth stores, issues and verifies the keys; your app decides what they may do.
What they are
If your product is B2B and built on tenants, your customers will ask for API keys: to drive your API from CI, to run their end-to-end tests, to script a nightly export. A tenant API key is that key. A member of a tenant creates it, it belongs to that tenant, and it carries a subset of the scopes your app offers.
A tenant API key never authenticates against any ProductCraft API. Your customer sends it to your API. Your backend asks Auth whether the key is active (one introspection call, which you cache), learns which tenant and scopes it carries, and authorises the request itself.
| Key | Prefix | Belongs to | Used for |
|---|---|---|---|
| Platform API key | pcft_live_ | A ProductCraft workspace (you) | Your backend calling ProductCraft APIs, including introspecting tenant API keys. |
| App API key | hdk_live_ / hdk_test_ | One Auth app (you) | Your backend calling the app's admin lane on Auth. |
| Tenant API key | pctk_live_ / pctk_test_ | One tenant of your app, created by one of its members | Your customer's CI, scripts and e2e tests calling YOUR app's API. |
Tenant API keys are off until you turn them on for an app. The adoption guide walks through enabling them, verifying them in your backend, and letting your customers manage them.
Format
pctk_live_4fQ2aZ0000000000000000000000000000000000000_1a2B3c
│ │ │ │
│ │ │ └─ 6 chars: CRC32 of the body, base62
│ │ └─ 43 chars: 32 random bytes, base62
│ └─ live (production app) or test (staging / development app)
└─ the same prefix for every app
Regex: ^pctk_(live|test)_[0-9A-Za-z]{43}_[0-9A-Za-z]{6}$One fixed prefix for every app means one pattern for your log redaction and for secret scanners. The environment comes from your app: a production app mints pctk_live_ keys, any other app mints pctk_test_.
The checksum is a typo filter, not a security control. Anyone can compute a valid checksum for a random body. Check it before you do anything else, so a mangled or truncated key is refused without a cache lookup or a network call, but never treat a valid checksum as evidence that a key exists. Flood protection is the job of your rate limits and of Auth's per-app introspection limit.
Auth keeps only a SHA-256 hash of the key. The secret is returned once, when the key is created or rotated, with Cache-Control: no-store. Afterwards a key is recognisable only by its key_prefix (the first 16 characters) and last4, and by its id, tak_….
Scopes and the ceiling
Your app publishes its grantable scopes: the permission strings a key may carry, such as disputes.read or reports.create (at most 64, each <resource>.<verb>). A key carries at least one of them.
A key can never do more than the person who created it. When a member creates or rotates a key, every requested scope must be grantable by the app and held by that member's own tenant role (a tenant owner can grant every grantable scope). Otherwise the request is refused with TENANT_API_KEY_SCOPE_ABOVE_CEILING and the offending scopes in details.scopes.
The ceiling keeps applying after the key exists. Introspection returns the creator's current tenant role, and your backend intersects the key's scopes with that role's permissions on every request. If the creator is demoted, their keys shrink with them. Role drift can only narrow what a key does.
Expiry
Every key expires. At creation the member picks 7, 30, 90 or 365 days; your app sets the default (90 unless you change it) and the maximum. There is no “never expires” option: these keys live in CI systems, and a forgotten one should die on its own.
Expiry is final. The key's expires_at is on every listing and in every introspection answer, so a customer can rotate before it lapses.
Rotate vs revoke
Rotate mints a replacement with the same name and scopes and returns its secret once. The outgoing key stops working at once (the default) or keeps working for a grace period of up to 24 hours, so a CI pipeline can switch over without a failed run. The person who rotates becomes the replacement's creator, so the ceiling is checked against their role, and the key now lives as long as their membership.
Revoke is immediate and final. Revoking a revoked key is a no-op that still succeeds.
If a key leaks, revoke it. Rotation with a grace period keeps the leaked key alive for that period; rotation exists for planned changes, not for incidents. Revoke the leaked key, then create a new one.
Revocation latency
A revocation takes effect in Auth the moment it is made: the next introspection of that key answers {"active": false}. Your backend caches positive answers for up to 30 seconds per server, so a revoked key can keep working against your API until the cached answer runs out. That window is your cache TTL, not a token lifetime: there is no signed token that outlives the revocation. Document it for your customers as “revocation takes effect within 30 seconds” (or whatever TTL you choose below that).
Orphaned keys
A key lives only as long as its creator's membership. When the creator is removed from the tenant or deletes their account, every key they created (or last rotated) is revoked in the same transaction, with reason member_removed. When the tenant is deleted, suspended or archived, its keys are revoked with reason tenant_deleted; reactivating the tenant does not bring them back.
Introspection re-checks membership and tenant status on every call, so a key whose eager revocation was missed is still refused. Such a key shows as orphaned.
Tell your customers what this means in practice: the CI key Alice created stops working the day Alice leaves the team. Keys that must outlive one person should be rotated by someone who is staying, which makes them the creator.
Key status
| Status | Meaning |
|---|---|
| active | Usable. Introspection answers with the tenant, scopes and creator. |
| expired | Past expires_at. Final: an expired key never comes back. |
| revoked | Revoked by a tenant member, by you in the console, by a rotation, or because its creator or tenant went away. Final, and it wins over expired. |
| orphaned | The creator is no longer a member of the tenant, or the tenant is not active. Introspection treats it as inactive. |
| suspended | Every key of the app is switched off (the console's suspend switch, the feature disabled, or the app itself not active). Reversible: keys come back when the switch is turned off. |
Introspection deliberately answers {"active": false} for every inactive reason, so a caller learns nothing by probing. Your backend can ask why a key it saw go inactive died, for its own audit trail, with GET /:appSlug/v1/tenant-api-keys/:keyId/status, which returns the status and the revoke_reason (manual, rotated, member_removed or tenant_deleted).
The suspend switch
Each app's Settings in the console has a Suspend all tenant API keys switch. It is your incident lever: while it is on, introspection answers {"active": false} for every key of the app and creating, rotating or revoking keys is refused with TENANT_API_KEYS_SUSPENDED. Nothing is revoked, so turning the switch off brings every key that is still within its expiry back.
The switch belongs to the console only. Your app pushes its key settings from code on boot, and that push can neither set nor clear the switch, so a deploy in the middle of an incident cannot undo it.
For a single tenant, open the app in the console, then Tenants, the tenant, and API keys: you can see every key (name, prefix and last four characters, scopes, creator, last used, expiry, status) and revoke any of them.
Who can manage keys
Only a signed-in end user can create, rotate or revoke a key, for the tenant they are currently signed into, and only if their tenant role is one of your app's manage_roles (by default only owner). An M2M token, a Platform API key or a tenant API key can never create a key, so a leaked key cannot mint its replacement. If your app sets a recent-MFA window, creating and rotating also require a second factor verified within that window.
Key changes are recorded in the app's audit log (tenant.api_key.created, .rotated, .revoked, .expired), each with the tenant it belongs to. Individual introspection calls are not logged; a key's last_used_at records use, to the nearest five minutes.
Limits
- 25 active keys per tenant.
- Key names are 1 to 64 characters.
- At most 64 grantable scopes per app.
- Rotation grace: 0 to 24 hours, default 0.
- Introspection: 1,200 calls a minute per app, separate from your Platform API key's other calls. Over it the answer is
429withRetry-After, never a false{"active": false}.