Skip to content

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": "..." }
  }
}
  • kind is what to do about it: VALIDATION, UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED and the others. It is the stable contract. Branch on kind.
  • code says what specifically failed. Codes are added and refined over time, so read code for diagnosis, not for control flow.
  • publicData and field are present only when the failure sets them. field names the rejected input.
  • correlationId identifies 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

EndpointWhat it does
GET /api/v1/meThe caller's own identity: principal, organisation, actor type, how it authenticated, roles and scopes. Any authenticated caller.
POST /api/v1/signupKeyless 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/verificationWhether 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/verificationSends 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-confirmationAn 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-approvalThe 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-recoveryAsks 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/completionSpends 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/keysLists 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}/rotateIssues 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/statusThe keyless trial's status. No credential. Returns a fixed status.
GET /api/v1/servicesEvery 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.