Skip to main content
An API key is a signing key pair bound to one tenancy. The Platform generates it, shows you the private half once, and keeps only the public half. Nothing secret ever travels. Every request carries a signature over what that request does, the moment, the method, the path, the body, and the Platform verifies it against the public half it stored.
The private half is shown exactly once, at minting. The Platform stores nothing that can sign, so a key you lose cannot be recovered. It is revoked and replaced.

What a key is

A minted key is three parts, separated by underscores.
Cut the parts by position, not by splitting on the underscore. Both halves are base64url, whose alphabet contains the underscore, a split comes apart on roughly three keys in four, depending on nothing but what the random source drew.

Signing a request

The signature is taken over a canonical string of five lines, joined by newlines with nothing after the last. There is no other canonicalisation. Query parameters are not reordered and nothing is normalised: you sign what you send and send what you signed. The signature is Ed25519 over that string, and the header presents it:
The signature is standard base64 without padding. The timestamp must be within 5 minutes of the Platform’s clock, either way, and the window binds however far outside it a stamp lies. A request stamped four centuries ahead is refused exactly as one stamped six minutes ahead is.

Test vectors

Check your signer against these before your first request. They are produced by the same code that verifies your requests, and the Platform’s own test fails if they drift. The key below is a seed of thirty-two zero bytes, so any Ed25519 library reproduces it. Signing POST /v1/tenants/capyplazo/clients with the body {"id":"client-0001"}, no idempotency token, at ts=1787000000:
Canonical string
The fifth line is empty, and the string ends there, no trailing newline. That empty line is the idempotency token this request does not carry.
Authorization header

A reference signer

Refusals

Every unusable presentation receives the same answer: 401, with WWW-Authenticate: Capcells-Ed25519 and the body {"error":"this route requires a signed credential","code":"unauthenticated"}. The same answer, byte for byte, for all of these: Nothing in the answer tells them apart, deliberately: a caller that could would learn which of its guesses was closer. The first thing to check is your clock, because it is the only one of these that can appear suddenly on a signer that worked yesterday. The last row is the one that reads as a puzzle, so it is worth stating plainly: an over-long key is refused as unauthenticated rather than as a bad key, because the door promises exactly two answers before any handler runs and a third would be a new thing to read. Keep keys short, a UUID is 36 bytes, and the case never arises. At the Console door, which carries no signature, the same key is 400 invalid_idempotency_key.
The 5-minute window bounds a replay, and what closes it is the act’s own idempotency rather than the window. A captured request replayed inside those five minutes reaches the route again, and on the two routes with no reference of yours — creating a Configuration, or a version of one — it performs the act a second time unless you sent an Idempotency-Key. The key is part of the signed string, so a captured request cannot have one added or changed; sending one is what makes the replay harmless. Retries carries the rule.

When the key is fine

A 403 is the other answer, and it means the opposite of a 401: the signature verified, the key is live, and the request is still refused. There are two, and they are separate codes because the remedies have nothing to do with each other. Neither names an identifier and neither names a scope. Which tenancy your key acts for is something you already know; which one you asked for is in your own URL. What the Platform will not do is answer “no” in a way that lets a caller discover other tenants by asking.

Rotating and revoking

Mint the new key first, prove it works, then revoke the old one. keys are independent, so both are live during the handover, the reverse order leaves a window where you cannot call anything. Revoking binds on the very next request. There is no window in which a revoked key still works.

Limits