# Using the Zaroz API

Everything the console does, you can do with the API. This page is the part that is the same for every route; the guides that follow (organizations, tokens, billing, domains, databases, backups, services) only describe what is specific to them.

All guides use the same three-line helper. Copy it once.

**curl**

```bash
export ZAROZ_API="https://kernel.zaroz.cloud/api/v2"
export ZAROZ_TOKEN="zaroz_..."          # see "Tokens" below
AUTH="Authorization: Bearer $ZAROZ_TOKEN"
```

**TypeScript**

```ts
// zaroz.ts (Node 20+, no dependencies)
const API = process.env.ZAROZ_API ?? "https://kernel.zaroz.cloud/api/v2";
const TOKEN = process.env.ZAROZ_TOKEN!;

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 (pip install requests)
import os
import requests

API = os.environ.get("ZAROZ_API", "https://kernel.zaroz.cloud/api/v2")
_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)
```

The full, always current list of routes, bodies and responses is the [API reference](/api). These guides are the part the reference cannot tell you: which call to make first, and why.

## Tokens

Send `Authorization: Bearer <token>` on every request. A script uses an **API token** you create in the console or with the API, scoped to exactly the permissions it needs; see [API tokens and permissions](/api-tokens/getting-started). A token never has more authority than the person (or organization) that made it.

## Pagination

List routes that can grow take `page` (starting at 1) and `page_size`, and answer an envelope:

```json
{
  "data": [ { "id": "..." } ],
  "pagination": {
    "page": 1, "page_size": 25, "total_items": 61, "total_pages": 3,
    "has_next_page": true, "has_previous_page": false
  }
}
```

Always follow `has_next_page` rather than guessing a page count. A list that is not paged (for example the members of an organization) answers a plain JSON array.

**curl**

```bash
curl -sS "$ZAROZ_API/subscriptions?page=1&page_size=50" -H "$AUTH"
```

**TypeScript**

```ts
import { call } from "./zaroz";

let page = 1;
const all: any[] = [];
for (;;) {
  const { body } = await call("GET", `/subscriptions?page=${page}&page_size=50`);
  all.push(...body.data);
  if (!body.pagination.has_next_page) break;
  page += 1;
}
console.log(all.length);
```

**Python**

```python
from zaroz import call

page, subscriptions = 1, []
while True:
    _, body = call("GET", f"/subscriptions?page={page}&page_size=50")
    subscriptions += body["data"]
    if not body["pagination"]["has_next_page"]:
        break
    page += 1
print(len(subscriptions))
```

## Errors

Every refusal has an HTTP status and a JSON body with a stable, kebab-case `code`:

```json
{
  "code": "member-outranks-caller",
  "description": "You cannot change a member whose role is above yours",
  "details": "member-outranks-caller: You cannot change a member whose role is above yours"
}
```

- **Switch on `code`, never on `description`.** The description is a sentence for people and may be reworded or translated; the code does not change.
- A `404` is also what you get for something you are not allowed to see, so that the API does not reveal what exists.
- `429` means slow down. Honor the `Retry-After` header.
- Refusals that a program can act on (the [Units API](/guides/units-api/errors)) also carry a `docs_url` pointing at the section that explains the code and what to do.
- A refusal is never a partial success: a request that fails changed nothing.

**TypeScript**

```ts
import { call } from "./zaroz";

const r = await call("PATCH", `/organizations/${ORG}/members/${USER}`, { role: "admin" });
if (r.status >= 400) {
  if (r.body.code === "member-outranks-caller") console.log("ask an owner to do this");
  else throw new Error(`${r.status} ${r.body.code}`);
}
```

**Python**

```python
status, body = call("PATCH", f"/organizations/{ORG}/members/{USER}", {"role": "admin"})
if status >= 400:
    if body["code"] == "member-outranks-caller":
        print("ask an owner to do this")
    else:
        raise RuntimeError(f"{status} {body['code']}")
```

## Idempotency

Routes that move money or create something you cannot easily undo (redeeming a wallet code, paying an invoice from the wallet, the Units allocate call) accept an `Idempotency-Key` header. Repeating the identical request with the same key answers with the first result instead of a refusal or a duplicate, so a client that timed out can simply try again. Use a fresh random value (a UUID) per intended action, and reuse it only when retrying that same action.

## What a deployment offers

Zaroz Cloud can be run by partners with a subset of the platform. A deployment may not have a container registry, object storage, a payment provider, Domain Connect or the unit log store configured. Ask it, without authentication:

**curl**

```bash
curl -sS "$ZAROZ_API/public/capabilities"
```

```json
{
  "container_registry": true,
  "object_storage_buckets": true,
  "domain_connect": true,
  "unit_log_store": true,
  "unit_metrics_store": true,
  "paypal": true,
  "mollie": true,
  "nowpayments": true,
  "mail": true,
  "web_push": true,
  "warden": true
}
```

**TypeScript**

```ts
const caps = await (await fetch(`${API}/public/capabilities`)).json();
if (!caps.object_storage_buckets) console.log("this deployment has no object storage");
```

**Python**

```python
import requests
caps = requests.get(API + "/public/capabilities", timeout=10).json()
if not caps["domain_connect"]:
    print("show the manual DNS instructions only")
```

A value of `false` means the feature is **absent** on that deployment, not broken:

- its routes answer **`501` with code `capability-not-available`**;
- its products are not in the catalog;
- an integration should not offer it at all.

A provider that cannot be reached at this moment also reports `false`. Cache the answer for a short time (a minute is plenty), not for the life of your program.

## Platform status

Two unauthenticated reads tell a program whether the platform itself is having a bad time, even when nobody can sign in. They are what the console's outage and maintenance banners use.

**curl**

```bash
curl -sS "$ZAROZ_API/public/incidents"      # [{"id","title","message","started_at"}], only incidents happening now
curl -sS "$ZAROZ_API/public/maintenances"   # [{"id","title","scheduled_at","expected_duration_minutes"}], soonest first
```

**TypeScript**

```ts
const incidents = await (await fetch(`${API}/public/incidents`)).json();
if (incidents.length) console.log("Platform incident:", incidents[0].title);
```

**Python**

```python
incidents = requests.get(API + "/public/incidents", timeout=10).json()
if incidents:
    print("Platform incident:", incidents[0]["title"])
```

Resolved incidents are never returned, and maintenance windows are those not cancelled and scheduled no earlier than 24 hours ago. Check these before paging someone about a failing deploy.

See [Sign in, sessions and account security](/guides/api/sign-in) for how to get a session, and [Notifications, live streams and support tickets](/guides/support/notifications-and-tickets) for being told when something happens.
