API keys
Mint, list, update, rotate and revoke the keys your customers present.
packages/api/src/resources/keys.controller.ts by packages/docs/tools/generate-reference.mjs. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.POST /v1/keys
Answers 201 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Body. Validated by this schema, from the control plane's own source:
const CreateKey = z.object({
project_id: z.uuid(),
tenant_id: z.uuid(),
env: Env,
name: Name.optional(),
scopes: Scopes.optional(),
meta: Meta.optional(),
/** Omitted inherits the project default; `[]` is "no limits on this key". */
rate_limits: RateLimits.optional(),
expires_at: Timestamp.optional(),
});GET /v1/keys
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Query. Validated by this schema, from the control plane's own source:
const ListKeys = z.object({
project_id: z.uuid().optional(),
tenant_id: z.uuid().optional(),
env: Env.optional(),
include_revoked: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),
limit: z.coerce.number().int().min(1).max(200).default(50),
});GET /v1/keys/:id
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
PATCH /v1/keys/:id
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
Body. Validated by this schema, from the control plane's own source:
const UpdateKey = z.object({
name: Name.nullable().optional(),
scopes: Scopes.optional(),
meta: Meta.optional(),
/** `null` puts the key back on the project default. */
rate_limits: RateLimits.nullable().optional(),
expires_at: Timestamp.nullable().optional(),
});POST /v1/keys/:id/rotate
Rotation with an overlap window (report section 7.4). The successor is a new key -- a rotation that reused the material would not be a rotation -- and the predecessor keeps working until the window closes, so a fleet can pick the new key up without a synchronised restart.
The window shortens an existing expiry but never extends one: rotating a key that already expires in an hour must not buy it another day.
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
Body. Validated by this schema, from the control plane's own source:
Report section 7.4: default 24 h, hard maximum 7 days.
const RotateKey = z.object({
overlap_hours: z.number().min(0).max(168).default(24),
});POST /v1/keys/:id/revoke
Idempotent on purpose. Revocation is what a customer reaches for during an incident; making the second call fail is hostile at exactly the wrong moment. A repeat is not a mutation, so it writes no audit row.
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
Query. Validated by this schema, from the control plane's own source:
Report section 2.6's revoke-and-wait, and the answer to the security-review
question every enterprise buyer asks. wait=true turns "we told your fleet"
into "your fleet told us", which is the difference between a promise and a
receipt.
const RevokeQuery = z.object({
wait: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),
});Body. Validated by this schema, from the control plane's own source:
const RevokeKey = z.object({ reason: z.string().min(1).max(500).optional() });Shared validators
Defined once in packages/api/src/validation.ts and used by the schemas above.
const Env = z.enum(['live', 'test']);const Meta = z.record(z.string(), z.unknown());const Name = z.string().min(1).max(200);The same bounds migration 0008's keyring.rate_limits_valid checks, so a bad
set is a 400 with a field path rather than a 500 wrapping a constraint
violation. The database keeps the check regardless: this API is not the only
thing that will ever write that column, and a limit is an authorisation
bound.
window_ms has a floor of one second because these counters are one network
hop away (report section 3.2): a 100 ms window enforced across an RTT is a
number we cannot honestly claim to hold. The ceiling is 31 days, which is the
longest period anyone means by "per month".
const RateLimits = z
.array(
z.object({
id: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/),
limit: z.number().int().min(1).max(1_000_000_000),
window_ms: z.number().int().min(1_000).max(2_678_400_000),
algorithm: z.enum(['sliding', 'fixed']),
scope: z.enum(['key', 'tenant']),
}),
)
.max(8)
.refine(
(rules) => new Set(rules.map((rule) => rule.id)).size === rules.length,
// Two limits of one name share a counter and disagree about its bound, and
// `RateLimit-Policy` is a structured-field dictionary that cannot express
// the duplicate at all.
{ message: 'rate limit ids must be unique' },
);const Scopes = z.array(z.string().min(1).max(200)).max(64);const Timestamp = z.iso.datetime({ offset: true });