Zaroz
API Tokens

Create and use a fine-grained API token

API tokens let scripts, bots and CI jobs call the Zaroz Cloud API without storing your account password or browser session. Every token is deny-by-default: you choose the exact permissions it has and, for a personal token, the exact resources it may touch.

The complete list of routes lives in the API reference. The conventions every call shares (base URL, errors, pagination) are in Using the Zaroz API.

Personal or organization token

Personal tokenOrganization token
Acts asYouThe organization
Scope of each permissionGlobal, one resource type, one resource, or a resource and its descendantsThe whole organization
Revoked whenYou revoke it, or you lose accessYou revoke it; with membership_tied also when its creator leaves
Created atPOST /me/api-tokensPOST /organizations/{id}/api-tokens (needs Owner or Admin)
Best forOne script, one service, least privilegeA team's CI or a bot that must outlive one person

A token never has more authority than the person or organization that made it, and it is checked again on every request: if you lose a role, your tokens lose it too.

Before you create one

Treat a token like a password. Anyone who gets it can do everything it allows until it expires or you revoke it.

  • Create a separate token for each integration.
  • Grant only the permissions the integration needs.
  • Prefer one service to all of them (personal tokens only).
  • Add an expires_at whenever the integration can rotate its secret.
  • Never put a token in frontend JavaScript, a mobile app, a public repository or a support message.

See what you may grant

You can only grant what you hold yourself. Ask the API, so you never hard-code a list:

curl

TerminalCode
curl -sS "$ZAROZ_API/me/api-token-capabilities" -H "$AUTH" # {"data":[{"capability":"services.read","admin_only":false}, ...]} # for an organization token curl -sS "$ZAROZ_API/organizations/$ORG/api-tokens/grantable-capabilities" -H "$AUTH" # {"data":[{"capability":"services.read"}, ...], "max_lifetime_days": null}

TypeScript

TypeScriptCode
import { call } from "./zaroz"; const mine = await call("GET", "/me/api-token-capabilities"); console.log(mine.body.data.filter((c: any) => !c.admin_only).map((c: any) => c.capability));

Python

Code
from zaroz import call _, mine = call("GET", "/me/api-token-capabilities") print([c["capability"] for c in mine["data"] if not c["admin_only"]])

The permissions that matter to most customers:

PermissionAllows
services.readEvery non-secret read of a service
services.powerStart, stop, restart
services.deploySet the source and create deployments, nothing else
services.manageChange the service (environment, backups, files, firewall...) and everything services.deploy does
services.consoleLogs stream, command console, SSH
services.files.readBrowse and download files
services.external_access.manageOpen and close public ports
services.schedules.manageSchedules
services.databases.readBrowse attached databases
organizations.read, projects.readRead organizations and projects
organizations.members.manageAdd, remove and change roles of members
organizations.manage, projects.manageRename an organization; create, rename and delete projects
organizations.purchaseBuy a subscription billed to the organization
organizations.wallet.spend, organizations.wallet.top_upPay invoices from the wallet; add funds
subscriptions.renewal.manage, subscriptions.cancel, subscriptions.resize.manageTurn renewal on or off; end a subscription; resize or change plan
organizations.api_tokens.manageCreate, list and revoke the organization's tokens
wallet.readRead wallet balances and entries

There are no wildcards: services.read does not imply services.power.

Create a personal token

Minting needs a signed-in session (the console, or the login routes), not another API token. The secret is returned once, in the form zaroz_pat_<id>_<secret>; only a digest is stored.

Each grant is a permission plus a scope:

Scope kindReaches
globalThe permission wherever you hold it
resource_typeEvery resource of one type (resource_type)
resourceExactly one resource (resource_type, resource_id)
resource_treeA resource and what descends from it (resource_type, root_resource_id)

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/me/api-tokens" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "status-bot", "expires_at": "2027-01-01T00:00:00Z", "authorization": {"grants": [ {"capability": "services.read", "scope": {"kind": "resource", "resource_type": "service_instance", "resource_id": "<service instance uuid>"}} ]} }' # {"token": {"id": "...", "name": "status-bot", "capabilities": [...]}, "secret": "zaroz_pat_..."} shown once

TypeScript

TypeScriptCode
import { call } from "./zaroz"; const created = await call("POST", "/me/api-tokens", { name: "status-bot", expires_at: "2027-01-01T00:00:00Z", authorization: { grants: [ { capability: "services.read", scope: { kind: "resource", resource_type: "service_instance", resource_id: SERVICE }, }, ], }, }); console.log(created.body.secret); // store it now; it cannot be shown again

Python

Code
from zaroz import call _, created = call("POST", "/me/api-tokens", { "name": "status-bot", "expires_at": "2027-01-01T00:00:00Z", "authorization": {"grants": [{ "capability": "services.read", "scope": {"kind": "resource", "resource_type": "service_instance", "resource_id": SERVICE}, }]}, }) print(created["secret"]) # store it now; it cannot be shown again

An empty grants list makes a token that authenticates and can do nothing. Absence denies.

Create an organization token

The caller must be an Owner or Admin of the organization. Capabilities apply to the whole organization.

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/api-tokens" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "ci-deploy", "lifetime": "organization_owned", "expires_at": "2027-01-01T00:00:00Z", "capabilities": ["services.deploy", "services.read"] }'

TypeScript

TypeScriptCode
const org = await call("POST", `/organizations/${ORG}/api-tokens`, { name: "ci-deploy", lifetime: "organization_owned", // survives the person who created it expires_at: "2027-01-01T00:00:00Z", capabilities: ["services.deploy", "services.read"], });

Python

Code
_, org_token = call("POST", f"/organizations/{ORG}/api-tokens", { "name": "ci-deploy", "lifetime": "organization_owned", "expires_at": "2027-01-01T00:00:00Z", "capabilities": ["services.deploy", "services.read"], })

lifetime defaults to membership_tied: the token is revoked automatically when its creator leaves the organization. Use organization_owned when the token must keep working after that. Staff-only permissions can never be put on an organization token.

Use it

Send it as a bearer token. This reads a service with a token that holds only services.read:

curl

TerminalCode
curl -sS "$ZAROZ_API/service-instances/$SERVICE" -H "Authorization: Bearer zaroz_pat_..." curl -sS "$ZAROZ_API/service-instances/$SERVICE/resources" -H "Authorization: Bearer zaroz_pat_..."

TypeScript

TypeScriptCode
// status.mjs: ZAROZ_TOKEN must be the bot's token; Node 20+ const API = "https://kernel.zaroz.cloud/api/v2"; const res = await fetch(`${API}/service-instances/${process.env.SERVICE}/resources`, { headers: { Authorization: `Bearer ${process.env.ZAROZ_TOKEN}` }, }); if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`); const { data } = await res.json(); for (const r of data) console.log(r.kind, r.role, r.status);

Python

Code
import os, requests API = "https://kernel.zaroz.cloud/api/v2" r = requests.get( f"{API}/service-instances/{os.environ['SERVICE']}/resources", headers={"Authorization": "Bearer " + os.environ["ZAROZ_TOKEN"]}, timeout=20, ) r.raise_for_status() for res in r.json()["data"]: print(res["kind"], res["role"], res["status"])

Try calling a route the token lacks (for example POST /service-instances/{id}/restart with the token above): it answers 403. That is the point.

Do not call the API directly from a public website

Browser code is visible to every visitor. Keep the token in a server, bot, CI secret store or another trusted backend.

Errors you should handle

  • 401: the token is missing, malformed, expired or revoked.
  • 403: the token, its owner or both lack the permission for that resource.
  • 404: the id is wrong, or the resource is deliberately hidden from this identity.
  • 429: wait for Retry-After and retry.
  • 5xx: temporary; retry a limited number of times and log the response without logging the token.

List, rotate and revoke

curl

TerminalCode
curl -sS "$ZAROZ_API/me/api-tokens" -H "$AUTH" # your live tokens: never the secret curl -sS "$ZAROZ_API/organizations/$ORG/api-tokens" -H "$AUTH" # the organization's tokens curl -sS -X DELETE "$ZAROZ_API/me/api-tokens/$TOKEN_ID" -H "$AUTH" # 204 curl -sS -X DELETE "$ZAROZ_API/organizations/$ORG/api-tokens/$TOKEN_ID" -H "$AUTH" # 204

TypeScript

TypeScriptCode
const live = await call("GET", "/me/api-tokens"); for (const t of live.body.data) console.log(t.name, t.last_used_at); await call("DELETE", `/me/api-tokens/${TOKEN_ID}`);

Python

Code
_, live = call("GET", "/me/api-tokens") for t in live["data"]: print(t["name"], t["last_used_at"]) call("DELETE", f"/me/api-tokens/{TOKEN_ID}")

To rotate: create the replacement, update the integration, check it works, then revoke the old one. Revoking is immediate, and revoking a personal token needs a signed-in session, not a token. Token-backed actions appear in the organization activity with the token's name, so give tokens names you will recognise during an incident.

Next

Last modified on