The REST API
Base path
The REST API is served under /api/v1. Every path on this page is relative to
the origin you are reading it from, so GET /api/v1/me is that origin's
/api/v1/me.
The envelope
A successful answer wraps its payload in data and its related links in
_links. Most answers carry a self link.
{
"data": { "...": "the resource" },
"_links": { "self": { "href": "/api/v1/..." } }
}
A failure answers with the matching HTTP status and an error body:
{
"error": {
"message": "What went wrong, for the caller",
"code": "UNAUTHENTICATED",
"kind": "UNAUTHENTICATED",
"correlationId": "<this response's id>",
"publicData": { "...": "data the failure chose to expose" },
"field": { "name": "...", "path": "..." }
}
}
kindis what to do about it:VALIDATION,UNAUTHENTICATED,FORBIDDEN,NOT_FOUND,CONFLICT,RATE_LIMITEDand the others. It is the stable contract. Branch onkind.codesays what specifically failed. Codes are added and refined over time, so readcodefor diagnosis, not for control flow.publicDataandfieldare present only when the failure sets them.fieldnames the rejected input.correlationIdidentifies the response. Quote it when you report a problem.- An internal fault says only
UNEXPECTED, whatever caused it.
Authentication
A request carries one of two credentials:
- An API key, for an agent:
Authorization: Bearer <apiKey>, exactly as it was issued. - A session cookie, for a signed-in person. The dashboard's requests carry it on their own.
A key that is present but invalid is refused with 401. It is never retried as
a session. An OAuth access token is not accepted here. How each credential is
issued is in authentication.
Endpoints
| Endpoint | What it does |
|---|---|
GET /api/v1/me | The caller's own identity: principal, organisation, actor type, how it authenticated, roles and scopes. Any authenticated caller. |
POST /api/v1/signup | Keyless sign-up. No credential. The body takes operatorEmail, your human's address, which is required and is mailed a link to confirm, and optionally name, keyName and expiresIn. A body without operatorEmail is refused with 400. Returns a new organisation, its first API key (once), its wallet, which starts empty, and verification, what became of the operator address. See Getting started and authentication. |
GET /api/v1/account/verification | Whether the caller's account has a verified address, and, for an agent's account, any request waiting as pending: the address it names, when it stops working, and its stage, approval (waiting on the confirmed operator address) or confirmation (waiting on the address named). Buying credit and enabling a service in full need a verified address. Any caller acting for its own account; not a trial or keyless caller. |
POST /api/v1/account/verification | Sends a verification link. For an agent's account the optional body is { "address": "<the operator's address>" }, needed the first time; without one, a fresh link goes out for the request still waiting. Once the operator address is confirmed, naming a different one answers approval-requested and mails the confirmed address for approval first; the new address gets its link only once that is approved. See authentication. For a person's account, send no address: the link goes to the address they sign in with. An address you send that is not a string (null included) refuses the request instead, with 400 and a validation error, and no link is sent. Any caller acting for its own account; not a trial or keyless caller. |
POST /api/v1/account/verification/operator-confirmation | An operator confirms an address an agent named. No credential: the body is { "token": "<the token from the link>" }, and the answer is 200 with previouslyVerified, whether the account already had a confirmed address, which this one replaces. The page the emailed link opens calls it. A link that is unknown, used, replaced or expired — or an approval link — is refused with 400 and VERIFICATION_LINK_INVALID, and a body that is too large with 413. Limited per client address, in one allowance with the approval endpoint below. |
POST /api/v1/account/verification/operator-approval | The confirmed operator address approves replacing it with the address its message names. No credential: the body is { "token": "<the approval token from the link>" }, and the answer is 200 with { "approved": true } and nothing else; the new address is then sent its own confirmation link. The page the emailed link opens calls it. A link that is unknown, used, replaced or expired — or a confirmation link — is refused with 400 and VERIFICATION_LINK_INVALID, and a body that is too large with 413. Limited per client address, in one allowance with the confirmation endpoint above. See authentication. |
POST /api/v1/account/key-recovery | Asks for a new key for an organisation that has lost its key. No credential. The body is { "address": "<the confirmed operator address>" }. The answer is 202 with { "accepted": true } whatever the address; if the address is the confirmed operator address of any agent account, it is mailed one recovery link per organisation. An address that is not a valid email address is refused with 400, a body that is too large with 413, and too many requests from your client address with 429. See authentication. |
POST /api/v1/account/key-recovery/completion | Spends a recovery link. No credential: the body is { "token": "<the token from the link>" }. Cancels every key the organisation had and answers with organizationId, the new plaintext apiKey (once), keyId and revokedCount, the number of keys cancelled; the answer is sent Cache-Control: no-store. A link that is unknown, used or expired is refused with 400 and KEY_RECOVERY_LINK_INVALID; when no key could be issued the answer is 503 and KEY_RECOVERY_FAILED. Usually the operator asks for a new link; but when the organisation's owner is no longer the agent account the operator address belongs to, a new link fails the same way, and the operator should contact support. Limited per client address. |
GET /api/v1/keys | Lists an organisation's API keys, never their key material. ?organizationId= is optional and defaults to the caller's own organisation. A signed-in person whose role allows it; an API key is refused with 401. |
POST /api/v1/keys/{keyId}/rotate | Issues a replacement key and revokes the old one. The answer carries the new plaintext key, once, with the new and old key ids. A signed-in person whose role allows it, or with an API key, its own key only: any other key id is answered 404. Rotating with an API key is limited to 10 an hour per organisation; beyond that the answer is 429. |
DELETE /api/v1/keys/{keyId} | Revokes a key. A signed-in person whose role allows it, or with an API key, its own key only: any other key id is answered 404. |
GET /api/v1/trial/status | The keyless trial's status. No credential. Returns a fixed status. |
GET /api/v1/services | Every service and its state for the caller's organisation. A signed-in person only; an agent uses services_list over MCP. |
GET /api/v1/services/{serviceId} | One service for the caller's organisation: its state, trial, documentation, prices and settings. A signed-in person only; an agent uses services_describe over MCP. |
A sample call:
GET /api/v1/me
Authorization: Bearer <apiKey>
Rate limits
POST /api/v1/signup, GET /api/v1/trial/status, both key-recovery
endpoints and the two operator endpoints are limited per client address; the
operator confirmation and approval endpoints share one allowance. Sign-up's limit is shared with the sign-up page, so sign-ups from one
address count against one allowance whichever way they came.
POST /api/v1/account/verification is limited per organisation, in one
allowance shared with the account_verification_request tool. Rotating a key
with an API key, POST /api/v1/keys/{keyId}/rotate, is limited to 10 an hour
per organisation. An API key can be limited
too. Over a limit the answer is 429 Too Many Requests with a Retry-After
header giving the seconds to wait, and kind RATE_LIMITED. Wait that long
before trying again.
Sign-up also has a cap across the whole platform: once the platform has
taken its hourly number of sign-up attempts, every sign-up is refused until
the hour turns, whatever your address. That refusal is 429 with Retry-After and the code
SIGNUP_PLATFORM_LIMIT_REACHED.
A sign-up refused because sign-up is closed is not a limit: it is 403 with
the code SIGNUP_DISABLED, and waiting will not change it.