# Operate a running service

A **service instance** is what a subscription runs: a game server, a website, a database, a Workload. This guide is the daily API for any of them. What a particular service supports depends on its product: read its [capabilities](/concepts/capabilities) first (`GET /service-instances/{id}/capabilities`), and expect `404` for a feature the product does not offer. The samples use the helper from [Using the Zaroz API](/guides/api/overview).

## Find your services

**curl**

```bash
curl -sS "$ZAROZ_API/service-instances?project_id=$PROJECT&page_size=50" -H "$AUTH"
curl -sS "$ZAROZ_API/service-instances/$SERVICE" -H "$AUTH"
curl -sS "$ZAROZ_API/service-instances/$SERVICE/resources" -H "$AUTH"
curl -sS -X PATCH "$ZAROZ_API/service-instances/$SERVICE" -H "$AUTH" -H "Content-Type: application/json" -d '{"name": "Survival"}'
```

**TypeScript**

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

const list = await call("GET", `/service-instances?project_id=${PROJECT}`);
for (const s of list.body.data) console.log(s.id, s.name, s.run_state, s.subscription_status);

const res = await call("GET", `/service-instances/${SERVICE}/resources`);
const main = res.body.data.find((r: any) => r.is_declared_primary);
const RESOURCE = main.id; // the id the logs, metrics and files routes use
```

**Python**

```python
from zaroz import call

_, listing = call("GET", f"/service-instances?project_id={PROJECT}")
for s in listing["data"]:
    print(s["id"], s["name"], s["run_state"], s["subscription_status"])

_, res = call("GET", f"/service-instances/{SERVICE}/resources")
RESOURCE = next(r["id"] for r in res["data"] if r["is_declared_primary"])  # used by logs, metrics and files
```

`run_state` is `not_provisioned`, `provisioning`, `running`, `stopping`, `suspended`, `crash_looping`, `degraded` or `failed`. It is read from the database, not from a live cluster call, so check `availability`: `cluster_unavailable` means it could not be refreshed right now. Renaming (`PATCH`) never changes the slug; a slug change is a separate, explicit field because it moves every URL pointing at the service.

## Power

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/start" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/stop" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/restart" -H "$AUTH"
```

**TypeScript**

```ts
const r = await call("POST", `/service-instances/${SERVICE}/restart`);
if (r.status === 409) console.log("busy:", r.body.code); // operation-in-progress, restore-in-progress
```

**Python**

```python
status, body = call("POST", f"/service-instances/{SERVICE}/restart")
if status == 409:
    print("busy:", body["code"])   # operation-in-progress, restore-in-progress
```

`start` and `stop` are explicit rather than a toggle, so two callers that both read "running" cannot cancel each other out. Stopping powers the service off and **keeps its volume**; it does not cancel or delete anything. A subscription that is suspended, cancelled, terminated or failed answers `404` here: pay what is owed first. While another operation holds the service you get `409 operation-in-progress` naming which operation and since when; during a backup restore the code is `restore-in-progress`. The needed permission is `services.power`.

## Logs

The API reads **the retained window** of one container's output, oldest first. An empty answer means "not in the retained window", never "it never happened"; the `coverage` field says in a sentence what was read, so show it beside the lines.

**curl**

```bash
L=/service-instances/$SERVICE/resources/$RESOURCE/logs
curl -sS "$ZAROZ_API$L?limit=200" -H "$AUTH"
curl -sS -G "$ZAROZ_API$L" -H "$AUTH" --data-urlencode "search=error" --data-urlencode "cursor=$NEXT"
```

**TypeScript**

```ts
let cursor: string | undefined;
do {
  const q = new URLSearchParams({ limit: "500", search: "error", ...(cursor ? { cursor } : {}) });
  const { body } = await call("GET", `/service-instances/${SERVICE}/resources/${RESOURCE}/logs?${q}`);
  for (const line of body.lines) console.log(line.at, line.message);
  cursor = body.next_cursor ?? undefined;
} while (cursor);
```

**Python**

```python
from urllib.parse import urlencode

cursor = None
while True:
    q = {"limit": 500, "search": "error", **({"cursor": cursor} if cursor else {})}
    _, body = call("GET", f"/service-instances/{SERVICE}/resources/{RESOURCE}/logs?{urlencode(q)}")
    for line in body["lines"]:
        print(line["at"], line["message"])
    cursor = body["next_cursor"]
    if not cursor:
        break
```

`search` is a case-insensitive substring, not a regular expression, and it runs on the server over the whole retained window. `cursor` is opaque and resumes exactly; never build one. `container` picks a container when the pod has several (`containers` lists them) and `previous=true` reads the previous run, handy after a crash. Lines carry no stream or level: nothing on the platform parses a severity.

To follow output **live**, mint a short-lived signed URL and open it: `POST .../logs/stream-ticket` returns `{ "url", "expires_at" }`. The grant is recorded in the organization's activity log. (For Workloads, see the longer retention in [Logs and metrics for units](/guides/units-api/logs-and-metrics).)

## Metrics

**curl**

```bash
curl -sS "$ZAROZ_API/service-instances/$SERVICE/resources/$RESOURCE/metrics?window_minutes=15" -H "$AUTH"
```

**TypeScript**

```ts
const m = await call("GET", `/service-instances/${SERVICE}/resources/${RESOURCE}/metrics?window_minutes=15`);
for (const s of m.body.series) console.log(s.id, s.limit, s.points.length); // cpu, memory, net_in, net_out
```

**Python**

```python
_, m = call("GET", f"/service-instances/{SERVICE}/resources/{RESOURCE}/metrics?window_minutes=15")
for s in m["series"]:
    print(s["id"], s["limit"], len(s["points"]))   # cpu, memory, net_in, net_out
```

`window_minutes` is `5`, `15` or `30` (the default); anything else is refused because retention is thirty minutes. Every window returns 60 buckets, so a bucket is 5, 15 or 30 seconds wide. A bucket with no samples has a `null` value so a chart can break the line. Each point is a pair of a timestamp and a value; series are keyed `cpu`, `memory`, `net_in`, `net_out`, and `limit` is the ceiling it is measured against (or `null` when there is none). Convert units yourself.

## Environment variables

**curl**

```bash
E=/service-instances/$SERVICE/environment
curl -sS "$ZAROZ_API$E" -H "$AUTH"
curl -sS -X PUT "$ZAROZ_API$E/DATABASE_URL" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"value": "postgres://…", "secret": true}'
curl -sS -X POST "$ZAROZ_API$E/DATABASE_URL/reveal" -H "$AUTH"      # recorded in the activity log
curl -sS -X DELETE "$ZAROZ_API$E/OLD_FLAG" -H "$AUTH"
```

**TypeScript**

```ts
const E = `/service-instances/${SERVICE}/environment`;
const set = await call("PUT", `${E}/LOG_LEVEL`, { value: "debug", secret: false });
console.log(set.body.applies, set.body.pending); // "restart" etc.
```

**Python**

```python
E = f"/service-instances/{SERVICE}/environment"
_, set_ = call("PUT", f"{E}/LOG_LEVEL", {"value": "debug", "secret": False})
print(set_["applies"], set_["pending"])   # "restart" etc.
```

One variable per call. A **secret**'s value is always `null` when you list: you can write or reveal it, never read it from the list. Sending a secret with no `value` keeps the stored one. `applies` is the platform's verdict on when the change reaches the running container: `immediately`, `restart` (a new pod is needed; today every change on a container app) or `deploy`. `pending` says whether the re-render really happened: if it did not (for instance the subscription is not active yet) the old value is still live, which matters when you rotate a leaked credential. A change takes a configuration lease (`409` while another operation holds the service) and shows in the deployment history as a `config_change`. `ZAROZ_AUTH_TOKEN` is reserved.

## Files

**curl**

```bash
F=/service-instances/$SERVICE/resources/$RESOURCE/files
curl -sS -G "$ZAROZ_API$F" -H "$AUTH" --data-urlencode "path=/data"
curl -sS -G "$ZAROZ_API$F/content" -H "$AUTH" --data-urlencode "path=/data/server.properties"
curl -sS -X PUT -G "$ZAROZ_API$F/content" -H "$AUTH" -H "Content-Type: application/json" \
  --data-urlencode "path=/data/motd.txt" -d '{"content": "Welcome\n"}'
```

**TypeScript**

```ts
const q = new URLSearchParams({ path: "/data/motd.txt" });
await call("PUT", `/service-instances/${SERVICE}/resources/${RESOURCE}/files/content?${q}`, { content: "Welcome\n" });
const back = await call("GET", `/service-instances/${SERVICE}/resources/${RESOURCE}/files/content?${q}`);
console.log(back.body.content);
```

**Python**

```python
from urllib.parse import urlencode

q = urlencode({"path": "/data/motd.txt"})
call("PUT", f"/service-instances/{SERVICE}/resources/{RESOURCE}/files/content?{q}", {"content": "Welcome\n"})
_, back = call("GET", f"/service-instances/{SERVICE}/resources/{RESOURCE}/files/content?{q}")
print(back["content"])
```

This is for **text** files up to **1 MiB** (a larger file is refused with a message naming the cap, never truncated, and a file that is not UTF-8 is refused for the same reason). The path is absolute within the volume mount and the server normalises it. For anything bigger, binary or many files at once, use SFTP (`GET /service-instances/{id}/sftp`, [mount it as a drive](/sftp/mount-as-drive)) or `POST .../files/download-url` for a signed download link.

## Scheduled tasks

A schedule runs a pipeline of tasks on a cron expression. Five fields (minute, hour, day of month, month, day of week), a time zone name and an overlap policy: `skip` (do not start while the last run is still going), `queue`, or `concurrent`. Tasks run in order and the first failure ends the run: `console_command`, `run_executable`, `power_action`, `create_backup`, `wait`, at most 25. This is the simplest way to get a **nightly backup**:

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/schedules" -H "$AUTH" -H "Content-Type: application/json" -d '{
  "name": "Nightly backup",
  "cron_expression": "0 4 * * *",
  "timezone": "Europe/Madrid",
  "overlap_policy": "skip",
  "timeout_seconds": 3600,
  "action": {"kind": "tasks", "tasks": [{"kind": "create_backup"}]}
}'
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/schedules/$SCHEDULE/runs" -H "$AUTH"    # run it now
curl -sS "$ZAROZ_API/service-instances/$SERVICE/runs" -H "$AUTH"
```

**TypeScript**

```ts
const s = await call("POST", `/service-instances/${SERVICE}/schedules`, {
  name: "Nightly backup",
  cron_expression: "0 4 * * *",
  timezone: "Europe/Madrid",
  overlap_policy: "skip",
  timeout_seconds: 3600,
  action: { kind: "tasks", tasks: [{ kind: "create_backup" }] },
});
console.log(s.body.next_fire_at, s.body.upcoming_fire_times);
```

**Python**

```python
_, s = call("POST", f"/service-instances/{SERVICE}/schedules", {
    "name": "Nightly backup",
    "cron_expression": "0 4 * * *",
    "timezone": "Europe/Madrid",
    "overlap_policy": "skip",
    "timeout_seconds": 3600,
    "action": {"kind": "tasks", "tasks": [{"kind": "create_backup"}]},
})
print(s["next_fire_at"], s["upcoming_fire_times"])
```

The scheduler reads the new row within about ten seconds; the answer already tells you `next_fire_at` and the next times, so you can check your expression. A cron-job service's schedules use `{"kind": "run_job"}` instead of tasks. Each run is recorded (also the ones that did not execute): `outcome` is `succeeded`, `failed`, `interrupted`, `skipped_overlap`, `skipped_condition` or `missed`. A schedule can also carry guard `conditions` (a time window, a calendar exclusion, the previous run, an HTTP check, another schedule's success) and be `paused`. See [Backups](/guides/backups/overview) for what a backup does.

## Activity

```bash
curl -sS "$ZAROZ_API/service-instances/$SERVICE/activity?page_size=25" -H "$AUTH"
```

```ts
const a = await call("GET", `/service-instances/${SERVICE}/activity?page_size=25`);
```

```python
_, a = call("GET", f"/service-instances/{SERVICE}/activity?page_size=25")
```

Newest first: what the owner did to this service (and by which person or token). Platform-internal events are excluded.

## Share a service with someone

Give a person access to **one service** without making them a member of the organization. They see only what you grant, cannot re-share, and never see other services or the organization's members. Permissions are a list of: `console`, `files`, `power`, `settings`, `databases`, `database_browse`, `database_rows`, `database_schema`, `external_access`, `schedules`, `billing` (seeing the service is implied by any share). `billing` reads invoices, resizes and withdraws a scheduled cancellation; it never cancels, renews or pays.

**curl**

```bash
S=/service-instances/$SERVICE/shares
curl -sS -X POST "$ZAROZ_API$S" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"email": "friend@example.com", "permissions": ["console", "power"]}'
curl -sS "$ZAROZ_API$S" -H "$AUTH"
curl -sS -X PATCH "$ZAROZ_API$S/$SHARE" -H "$AUTH" -H "Content-Type: application/json" -d '{"permissions": ["console"]}'
curl -sS -X DELETE "$ZAROZ_API$S/$SHARE" -H "$AUTH"
curl -sS "$ZAROZ_API/me/shared-service-instances" -H "$AUTH"       # what was shared with ME
```

**TypeScript**

```ts
const share = await call("POST", `/service-instances/${SERVICE}/shares`, {
  email: "friend@example.com",
  permissions: ["console", "power"],
});
console.log(share.body.status); // "pending" until they open "Shared with me"
```

**Python**

```python
_, share = call("POST", f"/service-instances/{SERVICE}/shares", {
    "email": "friend@example.com",
    "permissions": ["console", "power"],
})
print(share["status"])   # "pending" until they open "Shared with me"
```

Sharing needs the Owner or Admin role, and a product that offers it (`shareable` in its capabilities, otherwise `404`). The answer is the same `pending` share whether or not the address has an account, and `accepted` shows once they open it. Editing permissions takes effect on their next request. Refusals: `409` `already_shared`, `409` `share_limit_reached`, `422` `cannot_share_with_self`, and `429` past 20 new shares an hour per service.
