# 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](/api). The conventions every call shares (base URL, errors, pagination) are in [Using the Zaroz API](/guides/api/overview).

## Personal or organization token

| | Personal token | Organization token |
|---|---|---|
| Acts as | You | The organization |
| Scope of each permission | Global, one resource type, one resource, or a resource and its descendants | The whole organization |
| Revoked when | You revoke it, or you lose access | You revoke it; with `membership_tied` also when its creator leaves |
| Created at | `POST /me/api-tokens` | `POST /organizations/{id}/api-tokens` (needs Owner or Admin) |
| Best for | One script, one service, least privilege | A 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**

```bash
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**

```ts
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**

```python
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:

| Permission | Allows |
|---|---|
| `services.read` | Every non-secret read of a service |
| `services.power` | Start, stop, restart |
| `services.deploy` | Set the source and create deployments, nothing else |
| `services.manage` | Change the service (environment, backups, files, firewall...) and everything `services.deploy` does |
| `services.console` | Logs stream, command console, SSH |
| `services.files.read` | Browse and download files |
| `services.external_access.manage` | Open and close public ports |
| `services.schedules.manage` | Schedules |
| `services.databases.read` | Browse attached databases |
| `organizations.read`, `projects.read` | Read organizations and projects |
| `organizations.members.manage` | Add, remove and change roles of members |
| `organizations.manage`, `projects.manage` | Rename an organization; create, rename and delete projects |
| `organizations.purchase` | Buy a subscription billed to the organization |
| `organizations.wallet.spend`, `organizations.wallet.top_up` | Pay invoices from the wallet; add funds |
| `subscriptions.renewal.manage`, `subscriptions.cancel`, `subscriptions.resize.manage` | Turn renewal on or off; end a subscription; resize or change plan |
| `organizations.api_tokens.manage` | Create, list and revoke the organization's tokens |
| `wallet.read` | Read 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 `kind` | Reaches |
|---|---|
| `global` | The permission wherever you hold it |
| `resource_type` | Every resource of one type (`resource_type`) |
| `resource` | Exactly one resource (`resource_type`, `resource_id`) |
| `resource_tree` | A resource and what descends from it (`resource_type`, `root_resource_id`) |

**curl**

```bash
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**

```ts
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**

```python
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**

```bash
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**

```ts
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**

```python
_, 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**

```bash
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**

```ts
// 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**

```python
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.

:::danger{title="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**

```bash
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**

```ts
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**

```python
_, 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](/guides/organizations/members#activity-log) with the token's name, so give tokens names you will recognise during an incident.

## Next

- [Deploy from CI/CD](/guides/ci/overview): the narrow `services.deploy` token in practice.
- [Service principals](/guides/units-api/principals): credentials for code running inside a Workload, instead of a token of yours.
- [Build a Minecraft status monitor](/discord-bot/minecraft-status-monitor)
