#Auth pipeline: API key
#Purpose
Authenticate machine-to-machine callers with an API key instead of a user JWT. API-key mode is part of the built-in auth pipeline: auth: { mode: api-key, scopes: [...] } (§4.2, Decision 43), verified by the verify-api-key pipeline op (§2A.4).
#Natural-language source example
api/partners/_definition.md
# partners
Partner integration API. Only uses provider main.
All endpoints require an API key with partners:read scope.
api/partners/v1/list-orders/list-orders.md (route GET /partners/v1/list-orders)
# List partner orders
GET. Requires an API key with partners:read scope. At most 60 requests per minute per API key.
Query: page (integer, from 1) and page-size (integer, 1 to 100).
List orders with order.list and return 200 with items, page, page_size and has_more.
#Compiled lock md example
api/partners/_definition.lock.md (sections only; kind: api, scope: definition)
## Providers
- allowlist: [main]
- default: main
## Pipelines
### p1 auth
- mode: api-key
- scopes: [partners:read]
## Defaults
- timeout: 10s
- body: 1MB
api/partners/v1/list-orders/list-orders.lock.md (sections only; kind: api, scope: feature)
## Route
- method: GET
- path: /partners/v1/list-orders
## Access
- public: false
- public-writes: false
## Pipelines
### p1 auth
- mode: api-key
- scopes: [partners:read]
### p2 rate-limit
- per-minute: 60
- key: api-key
## Limits
- provider: main
- timeout: 10s
- body: 1MB
## Request
| field | type | rules | required |
| --- | --- | --- | --- |
| query.page | integer | min: 1 | no |
| query.page-size | integer | min: 1, max: 100 | no |
## Response
| status | schema | headers |
| --- | --- | --- |
| 200 | { items, page, page_size, has_more } | |
## Transform
- in: { page: req.query.page, page-size: req.query.page-size }
- out: { items: r.items, page: r.page, page_size: r.page_size, has_more: r.has_more }
## Steps
### s1 method
- op: method
- target: order.list
- args: transform.in
- out: r
- source-lines: 4-4
### s2 respond
- op: respond
- status: 200
- value: transform.out
- source-lines: 4-4
## Errors
| status | code | from |
| --- | --- | --- |
| 422 | VALIDATION_FAILED | VALIDATION_FAILED |
#Security considerations
Platform rules (§4.2, Decision 43):
- Transport: header
X-API-Key: <key>only. A key in the query string or body is rejected with 400 and never logged. - Format:
mbk_{env}_{key_id}_{secret};envisliveortest;key_idis 12 lowercase base32 characters;secretis 32 random bytes in base62.testkeys cannot be used in the live environment. - Scopes:
{definition}:{read|write|admin}or{definition}:*;adminimplieswrite,writeimpliesread; a global*is not allowed. Changing API-key scopes is a high-risk security item (§2A.5). - Storage: only
HMAC-SHA-256(pepper, secret)is stored (pepper in platform KMS); lookup bykey_id, then constant-time comparison. - Lifetime and rotation: default 90 days, maximum 365; at most 2 active keys per client; the old key overlaps the new one for 7 days by default (max 30), then expires.
- Revoke: platform UI/CLI
api-key revoke {key_id}; effective on all instances within 30 seconds; recorded by UPDATE ofrevoked_at, never DELETE. last_used_atis recorded; keys and secrets are never logged.- Context:
ctx.tenant_idcomes from the tenant the key is bound to;ctx.useris{ type: api-key, key_id, scopes }(§4.2). - Rate limiting per key uses
key: api-key(§4.3). - Privacy deletion revokes all API keys of the erased subject (§12.10 step 5).
- Missing/invalid key → 401; insufficient scope → 403 (§8).
#Spec gaps
- §8 requires
WWW-Authenticateper RFC 6750 (a Bearer scheme) on every 401, which does not obviously fit API-key auth; the challenge value for API-key mode is not specified. - How API keys are issued (UI/CLI command names other than
revoke) is not specified. - Whether
scopesdeclared in_definitionand in a feature are merged or overridden by inheritance (§3) is not specified.