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.
curl -X POST "$API/v1/token" \ -d grant_type=client_credentials \ -d client_id="$CLIENT_ID" \ -d client_secret="$CLIENT_SECRET"
Body
| Field | Type | Description | |
|---|---|---|---|
| grant_type | form | required | Always client_credentials. Form encoded, not JSON — so an OAuth library can point straight at this. |
| client_id | form | required | From the console's API access page. May instead go in an HTTP Basic header; both are legal and libraries differ. |
| client_secret | form | required | Shown once at creation. Rotating issues a new one and invalidates the old immediately. |
Responses
| Status | Meaning |
|---|---|
| 200 | The issuer's own response, including expires_in. Refreshing stays your library's job. |
| 400 | invalid_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. |
| 401 | invalid_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. |
| 502 | temporarily_unavailable: the identity provider could not be reached, so the credential was never judged. Retry; rotate nothing. |
| 503 | This 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.
curl "$API/v1/auth"
Responses
| Status | Meaning |
|---|---|
| 200 | The contract. No credential needed — this is how a client bootstraps. |
| 503 | This 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.
curl "$API/v1/projects/salmon/service-users" \ -H "Authorization: Bearer $TOKEN"
Responses
| Status | Meaning |
|---|---|
| 200 | The identities issued for this project. Never their secrets — those exist only at creation. |
| 401 | No token, or one that did not verify. |
| 403 | Not a tenant admin, or a credential scoped to another project. |
| 404 | No such project in your tenant. |
| 501 | This 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.
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
| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Identifies it in the list and in the audit log. Name the backend it belongs to. |
| description | string | optional | What it is for. Shown beside it in the console. |
| admin | boolean | optional | The 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
| Status | Meaning |
|---|---|
| 201 | Created. |
| 200 | A 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. |
| 400 | No name. |
| 401 | No token, or one that did not verify. |
| 403 | Not a tenant admin, or a credential scoped to another project. |
| 404 | No such project in your tenant. |
| 502 | The 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.
curl -X POST "$API/v1/projects/salmon/service-users/$USER_ID/secret" \ -H "Authorization: Bearer $TOKEN"
Responses
| Status | Meaning |
|---|---|
| 201 | The new secret, once. |
| 401 | No token, or one that did not verify. |
| 403 | Not a tenant admin, or a credential scoped to another project. |
| 404 | No such project in your tenant. An unknown user_id is not a 404 — see 502. |
| 502 | The 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.
curl -X DELETE "$API/v1/projects/salmon/service-users/$USER_ID" \ -H "Authorization: Bearer $TOKEN"
Responses
| Status | Meaning |
|---|---|
| 204 | Deleted. |
| 401 | No token, or one that did not verify. |
| 403 | Not a tenant admin, or a credential scoped to another project. |
| 404 | No such project in your tenant. |
| 502 | The identity provider refused — including for an unknown user_id. |