# Units API: overview and authentication

A **Workload** is your app on Zaroz. It runs as one or more **units** (copies). The Units API is how you, or the code inside your own Workloads, decide which units exist: create one by name, hand a warm one to a match, stop one that nobody uses, and watch what happened.

Every page in this guide has the same three samples for each operation: `curl`, TypeScript and Python. They assume the small helper below and nothing else. Copy it once.

**curl**

```bash
export ZAROZ_API="https://kernel.zaroz.cloud/api/v2"
export ZAROZ_TOKEN="zaroz_..."          # see "Authentication"
export PROJECT="<project uuid>"
export SERVICE="<service instance uuid>"
UNITS="$ZAROZ_API/projects/$PROJECT/service-instances/$SERVICE/units"
AUTH="Authorization: Bearer $ZAROZ_TOKEN"
```

**TypeScript**

```ts
// zaroz.ts: the only helper every sample on these pages relies on (Node 20+, no dependencies).
const API = process.env.ZAROZ_API ?? "https://kernel.zaroz.cloud/api/v2";
const TOKEN = process.env.ZAROZ_TOKEN!;
export const PROJECT = process.env.PROJECT ?? "";
export const SERVICE = process.env.SERVICE ?? "";
export const UNITS = `/projects/${PROJECT}/service-instances/${SERVICE}/units`;

export async function call<T = any>(
  method: string,
  path: string,
  body?: unknown,
  headers: Record<string, string> = {},
): Promise<{ status: number; body: T }> {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
      ...headers,
    },
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  const text = await res.text();
  return { status: res.status, body: (text ? JSON.parse(text) : null) as T };
}
```

**Python**

```python
# zaroz.py: the only helper every sample on these pages relies on (pip install requests).
import os
import requests

API = os.environ.get("ZAROZ_API", "https://kernel.zaroz.cloud/api/v2")
PROJECT = os.environ.get("PROJECT", "")
SERVICE = os.environ.get("SERVICE", "")
UNITS = f"/projects/{PROJECT}/service-instances/{SERVICE}/units"
_session = requests.Session()
_session.headers["Authorization"] = "Bearer " + os.environ["ZAROZ_TOKEN"]


def call(method, path, body=None, **kwargs):
    """Returns (status, parsed JSON or None)."""
    r = _session.request(method, API + path, json=body, timeout=40, **kwargs)
    return r.status_code, (r.json() if r.content else None)
```


:::note
Every route in this guide is in the kernel and in the API reference: list, policy, quote, logs, metrics and decisions; create by key, start, stop, destroy, labels and allocate; events; webhooks (create, update, delete, enable, test, rotate-secret, deliveries); service principals and their credentials; and the `replace_idle` and `next_start` rollouts. The one exception is `GET .../units/webhooks/metrics`, which the kernel serves but the reference does not list yet.
:::

## Where a request goes

Every Units route lives under one service:

```
/api/v2/projects/{project_id}/service-instances/{id}/units...
```

`{project_id}` and `{id}` are UUIDs: the project, and the Workload (a service instance). Both are in the console URL of the Workload page.

| Route family | Page |
|---|---|
| `POST .../units`, `start`, `stop`, `DELETE .../units/{key}` | [Create by key and the lifecycle](/guides/units-api/keys-and-lifecycle) |
| `PATCH .../units/{key}`, `user_data`, list filters | [Labels, user data and filters](/guides/units-api/labels-and-user-data) |
| `PATCH .../units/policy`, `POST .../units/allocate` | [The warm buffer and allocate](/guides/units-api/warm-buffer-and-allocate) |
| `GET .../units/events` | [Events](/guides/units-api/events) |
| `.../units/webhooks` | [Webhooks](/guides/units-api/webhooks) |
| `.../principal` | [Service principals](/guides/units-api/principals) |
| `GET .../units/logs`, `.../units/metrics` | [Logs and metrics](/guides/units-api/logs-and-metrics) |
| every refusal | [Errors](/guides/units-api/errors) |

## Authentication

Send a bearer token on every request: `Authorization: Bearer <token>`. Three kinds of token can call these routes.

| Caller | Token | How you get it |
|---|---|---|
| You, from a script or CI | A personal API token, scoped to the service | [CI/CD guides](/guides/ci/overview) show how to mint one with only the permissions you need |
| Code **inside** a unit of your Workload | The service principal token mounted at `/zaroz/credentials/token` | [Service principals](/guides/units-api/principals) |
| A program **outside** Zaroz (a matchmaker, a scheduler) | An external credential of the principal, `zaroz_sp_...` | [Service principals](/guides/units-api/principals) |

A principal can only use the unit routes it was granted (`units.read`, `units.control`). It can never change the scaling policy, the webhooks or its own grants: those stay with people.

## Conventions

- **Bodies are JSON** and unknown fields are refused (`400`), so a typo is an error, not a silent no-op.
- **Keys** are 1 to 40 characters, lowercase letters, digits and hyphens, starting and ending with a letter or digit. `u7`-style keys, keys starting `w-` or `zaroz`, and the words the routes use (`allocate`, `events`, `policy`) are reserved.
- **Labels**: at most 16 pairs, replaced whole. **`user_data`**: any JSON up to 64 KiB, 32 levels deep. It is **not a secret store**: every reader of the unit sees it. Secrets go in the Workload's environment.
- **Rate limit** for a service principal: 50 requests per second. A `429` carries `Retry-After`.
- **Errors** always carry a stable `code` and a `docs_url`. See [Errors](/guides/units-api/errors).
- **Billing**: a created, warm or allocated unit is an ordinary unit. It bills like any extra unit, per hour, with a 5-minute minimum per start. A stopped unit costs nothing. Events, webhooks and principals are free.

## The unit routes only work in `code` mode

`create`, `start`, `stop`, `destroy` and `allocate` answer `409 policy-not-code` unless the Workload's scaling policy has `mode: "code"`, because in `fixed` and `autoscaled` modes the platform decides which units exist. Switch the mode on the [warm buffer page](/guides/units-api/warm-buffer-and-allocate). Reading, listing and `PATCH` labels work in every mode.

## Try it end to end

The [example repositories](/guides/ci/overview#example-repositories) run every call in this guide against a real project.
