Open the console

API

Authentication

Getting a token, and managing the machine identities that earn one. Create a service user on a project's API access page, then exchange its id and secret here.

You send a client id and secret; you never send a key

The exchange yields a token that lasts about an hour. A captured request therefore carries something that expires rather than a permanent credential, and no service has to resolve a key on the path of every call.

The samples use $API and $TOKEN, which the API overview sets.

Exchange a client id and secret for a token

POST/v1/tokendeploy-service

The standard client-credentials grant, served on this host rather than the issuer's. Send an id and a secret, get a bearer token back.

This exists so you never have to know about the issuer

The identity provider addresses a token to a relying party through a scope, and getting it wrong yields a credential that is perfectly valid and refused by every route with a 401 that names nothing. The scope is added on this side; you send an id and a secret.

Request
curl -X POST "$API/v1/token" \
  -d grant_type=client_credentials \
  -d client_id="$CLIENT_ID" \
  -d client_secret="$CLIENT_SECRET"

Body

FieldTypeDescription
grant_typeformrequiredAlways client_credentials. Form encoded, not JSON — so an OAuth library can point straight at this.
client_idformrequiredFrom the console's API access page. May instead go in an HTTP Basic header; both are legal and libraries differ.
client_secretformrequiredShown once at creation. Rotating issues a new one and invalidates the old immediately.

Responses

StatusMeaning
200The issuer's own response, including expires_in. Refreshing stays your library's job.
400invalid_request — most often because you sent JSON; this route is form encoded. Or unsupported_grant_type — only client_credentials; a PERSON signs in through the console or the CLI's login.
401invalid_client. A missing id or secret, an unknown id and a wrong secret all answer this, so it cannot be used to probe which credentials exist.
502temporarily_unavailable: the identity provider could not be reached, so the credential was never judged. Retry; rotate nothing.
503This platform has no issuer configured.

Response body

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Discover where to sign in

GET/v1/authdeploy-service

The issuer, the client ids and the finished token scope — asked for rather than configured, so a client has one address to know.

A backend does not need this

token_scope is for OIDC clients — the console and the CLI sign a PERSON in against the provider directly, because a browser has to land on a login page. If you hold a client id and secret, POST /v1/token is your whole story and none of this applies.

Request
curl "$API/v1/auth"

Responses

StatusMeaning
200The contract. No credential needed — this is how a client bootstraps.
503This platform has no sign-in configuration yet. Said plainly rather than answering blanks a client would try to use.

List a project's service users

GET/v1/projects/{project}/service-usersdeploy-service

What machine identities exist for this project, and what each is for.

Tenant admin, including the list

A machine identity IS access to this project, so who holds one is as sensitive as creating one — and a list is how somebody discovers what to try to steal.

Request
curl "$API/v1/projects/salmon/service-users" \
  -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
200The identities issued for this project. Never their secrets — those exist only at creation.
401No token, or one that did not verify.
403Not a tenant admin, or a credential scoped to another project.
404No such project in your tenant.
501This deployment has no identity provider configured.

Response body

{
  "service_users": [
    {
      "user_id": "…",
      "username": "acme-backend",
      "display_name": "acme-backend",
      "description": "billing sync",
      "state": "active",
      "project_slug": "agents"
    }
  ]
}

Create a service user

POST/v1/projects/{project}/service-usersdeploy-service

Mints a client id and secret for one project. The same thing the console's New service user button does.

The secret is returned once and never again

Nothing stores it in a form it can be read back from. Creating a name that already exists ROTATES that identity rather than adding a second — which keeps an audit log readable and means a careless repeat invalidates the secret the first one is still using.

A name is looked up across the whole tenant, not the project

Names are unique per tenant. Creating acme-backend in project B when project A already has an acme-backend does not make a second one: it finds A's, re-scopes it to B and rotates its secret — so whatever A was running with stops working and the identity now belongs to B. Give each project's service users their own names.

admin: false on a repeat does not take the role away

The role is granted when asked for and never revoked by this route. To end write access, delete the identity and create a new one without it.

Request
curl -X POST "$API/v1/projects/salmon/service-users" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "acme-backend", "description": "billing sync", "admin": true}'

Body

FieldTypeDescription
namestringrequiredIdentifies it in the list and in the audit log. Name the backend it belongs to.
descriptionstringoptionalWhat it is for. Shown beside it in the console.
adminbooleanoptionalThe tenant admin role, shown in the console as "Can write". Needed to credit or freeze a wallet, mint a ticket, invoke for an end user, destroy, or manage service users. NOT needed to deploy, roll back or set secrets — an identity without it can still do those.

Responses

StatusMeaning
201Created.
200A service user of that name already existed in the tenant, so it was ROTATED — and re-scoped to this project. You still leave with a working credential.
400No name.
401No token, or one that did not verify.
403Not a tenant admin, or a credential scoped to another project.
404No such project in your tenant.
502The identity provider refused.

Response body

{
  "user_id": "…",
  "username": "acme-backend",
  "project_slug": "agents",
  "client_id": "…",
  "client_secret": "…",
  "created": true,
  "secret_shown_once": true
}

Rotate a secret

POST/v1/projects/{project}/service-users/{user_id}/secretdeploy-service

Issues a new secret for an existing identity and invalidates the old one.

Immediate, with no overlap

There is no grace period in which both work, so anything still holding the old secret stops at the moment you rotate. Deploy the new one first where you can, or accept the gap.

The id is not checked against the project in the path

The project must be yours and the credential's, but user_id is looked up across the whole tenant — so this rotates whichever service user the id names, in any project.

Request
curl -X POST "$API/v1/projects/salmon/service-users/$USER_ID/secret" \
  -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
201The new secret, once.
401No token, or one that did not verify.
403Not a tenant admin, or a credential scoped to another project.
404No such project in your tenant. An unknown user_id is not a 404 — see 502.
502The identity provider refused — which is also what an unknown user_id gets.

Response body

{
  "user_id": "…",
  "client_id": "…",
  "client_secret": "…",
  "secret_shown_once": true
}

Delete a service user

DELETE/v1/projects/{project}/service-users/{user_id}deploy-service

Removes the identity. Every token and secret it holds stops working.

The way to end a credential's access for good

Rotating replaces a secret; deleting ends the identity. Like rotating, the id is looked up across the whole tenant rather than checked against the project in the path.

Request
curl -X DELETE "$API/v1/projects/salmon/service-users/$USER_ID" \
  -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
204Deleted.
401No token, or one that did not verify.
403Not a tenant admin, or a credential scoped to another project.
404No such project in your tenant.
502The identity provider refused — including for an unknown user_id.

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.