Base64SignedPayload envelope and authenticate with a
header triple called SessionSig. You sign a canonical per-endpoint message
with your Ed25519 session key and send the signature in three headers.
You sign a canonical message built from the request’s fields, not the JSON
body. The body is parsed separately, and signing it instead fails verification
with 401. The exact byte layout is in What you sign.
When to use SessionSig
SessionSig covers API-key management and device login: the body is JSON, but the action still needs a session signature.| Endpoint | Action |
|---|---|
GET /api/v1/api-keys | List your API keys (prefixes only) |
POST /api/v1/api-keys | Create an API key |
POST /api/v1/api-keys/{id}/delete | Delete an API key |
POST /api/v1/login | Mint a device key (device login) |
X-API-KEY / X-DEVICE-KEY
header credentials.
The signing key is an Ed25519 session key. Minting an
admin-scope (unpinned) API key requires an admin-rooted session.
The header triple
Send three headers. All values use standard base64 (+ / / alphabet, =
padding); URL-safe base64 is rejected.
| Header | Contents |
|---|---|
X-PUBLIC-KEY | Base64 of the 32-byte Ed25519 session public key the exchange verifies against |
X-SIGNATURE | Base64 of the 64-byte Ed25519 signature over the canonical message (the canonical bytes, not the JSON body and not the base64 text) |
X-REQUEST-ID | A UUIDv7. It is both the freshness check and the idempotency key; its embedded timestamp must be current (see Clock skew) |
What you sign
Each endpoint defines an exact byte sequence, the canonical message. Build it from the request’s own fields, sign those bytes with the Ed25519 session key, and put the signature inX-SIGNATURE.
In every message, request_id is the raw 16 bytes of the X-REQUEST-ID UUIDv7,
integers are little-endian, and strings are UTF-8 with no terminator.
| Endpoint | Canonical message |
|---|---|
GET /api/v1/api-keys | request_id (16) ‖ account_id (8 LE) |
POST /api/v1/api-keys | request_id (16) ‖ account_id (8 LE) ‖ subaccount_or_max (4 LE) ‖ key_name (UTF-8) |
POST /api/v1/api-keys/{id}/delete | request_id (16) ‖ account_id (8 LE) ‖ api_key_id (16 raw bytes from the URL) |
POST /api/v1/login | request_id (16) ‖ account_id (8 LE) ‖ subaccount_or_max (4 LE) ‖ "device-login" |
subaccount_or_max is the subaccount index for a pinned credential, or the
maximum 32-bit value as the sentinel for an unpinned (admin-scope) credential. The
trailing "device-login" on POST /api/v1/login is that literal ASCII string.
Procedure
Generate a UUIDv7
Mint a fresh UUIDv7 at send time. Its embedded timestamp must be current when
the exchange receives the request.
Build the canonical message
Concatenate the fields for the endpoint, in order, using the raw 16-byte
request id and little-endian integers.
Clock skew
TheX-REQUEST-ID UUIDv7 must carry a current timestamp. The exchange rejects a
request whose timestamp is too old or too far in the future, returning 400 with
code request_timestamp_skew. Generate the UUIDv7 at send time and keep your clock
in sync (NTP).
Idempotency
X-REQUEST-ID is also the dedup key. Reusing the same UUIDv7 returns the original
result instead of acting twice. After a 500, 503, 504, or a network error,
retry with the same X-REQUEST-ID; the request is deduped, not executed
twice.
Read the body
These endpoints return their own JSON response, and a200 OK is not proof of
success. Always read the body, per the error model. The
HTTP status codes and retry semantics live there and in
error codes; the 429 / 413 limits are in
rate limits.
Common mistakes
Before you ship a SessionSig client, walk the common signing mistakes checklist. The frequent SessionSig failures:- Signing the JSON body instead of the canonical message.
- Using URL-safe base64 in a header. Use standard base64.
- Using a non-UUIDv7
X-REQUEST-ID— the exchange needs the embedded timestamp. - Sending the wrong
subaccount_or_maxsentinel for the scope you want. - Sending a
request_idwhose timestamp is not current.