# PoisonZero Admin API (v1)

Manage your daemon fleet programmatically: create app IDs with enrollment
codes, list your fleet, deactivate or delete apps. Built for scripts and AI
agents — one static API key, plain JSON, no SDK required.

Base URL: `https://poisonzero.com/api`
OpenAPI spec: `https://poisonzero.com/docs/api/openapi.json`
HTML docs: `https://poisonzero.com/docs/api`

## Authentication

Create an API key in the panel at <https://app.poisonzero.com/settings>
(Settings → API keys). The key (`pz_live_…`) is shown once — store it safely.
Keys are scoped to your account and can be revoked in the panel at any time.

Send the key as a Bearer token with every request:

    curl https://poisonzero.com/api/v1/apps \
      -H "Authorization: Bearer pz_live_..."

## Endpoints

### POST /v1/apps

Creates an app and an enrollment code in one call.

Request body (optional): `{"name": "build-server-07"}` — max. 100 characters.

Response `201`:

    {
      "appId": "k7x2m9q4w1bz8r3tj6vn",
      "name": "build-server-07",
      "status": "pending",
      "enrollCode": "mq4xw7k2p9t3vz8c5n1rb6dh",
      "enrollCodeExpiresAt": "2026-07-04T16:00:00.000Z"
    }

Use `appId` + `enrollCode` to enroll the daemon on the target machine
via the one-line installer (see <https://poisonzero.com/download>):

    PZ_APP_ID=<appId> PZ_ENROLL_CODE=<enrollCode> sh -c "$(curl -fsSL https://poisonzero.com/install.sh)"

### GET /v1/apps

Lists your fleet. Response `200`:

    {
      "apps": [
        {
          "appId": "k7x2m9q4w1bz8r3tj6vn",
          "name": "build-server-07",
          "status": "active",
          "platform": "linux",
          "agentVersion": "0.3.0",
          "lastSeenAt": "2026-06-04T15:00:00.000Z",
          "createdAt": "2026-06-01T09:00:00.000Z"
        }
      ]
    }

`status` is one of `pending` (created, not yet enrolled), `active`
(daemon enrolled and allowed), `revoked` (deactivated).

### GET /v1/apps/{appId}

Returns a single app (same fields as the list). Unknown IDs — including apps
that belong to a different account — answer `404`.

### POST /v1/apps/{appId}/enroll-code

Creates a fresh enrollment code for an existing app (e.g. to re-provision a
machine). Codes are single-use and valid for 30 days. Response `201`:

    { "enrollCode": "mq4xw7k2p9t3vz8c5n1rb6dh", "expiresAt": "2026-07-04T16:00:00.000Z" }

### POST /v1/apps/{appId}/revoke

Deactivates an app: status becomes `revoked` and the daemon's credentials stop
working immediately. Response `200`:

    { "appId": "k7x2m9q4w1bz8r3tj6vn", "status": "revoked" }

### DELETE /v1/apps/{appId}

Deletes an app permanently: app, configuration, daemon identity, open
enrollment codes, and its review-queue entries. Audit logs are retained.
Response `204` (empty body).

## Bulk provisioning

Each create call returns everything a machine needs to enroll:

    for i in $(seq -w 1 50); do
      curl -s -X POST https://poisonzero.com/api/v1/apps \
        -H "Authorization: Bearer $PZ_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"name\": \"daemon-$i\"}"
    done

## Errors

All errors share one shape:

    { "error": { "code": "...", "message": "..." } }

| Status | Code | Meaning |
|---|---|---|
| 401 | `invalid_key` | missing, malformed, unknown, or revoked API key |
| 401 | `account_disabled` | your account is disabled |
| 404 | `not_found` | unknown endpoint, or unknown/foreign app ID |
| 400 | `invalid_name` | `name` is not a string or exceeds 100 characters |
| 400 | `limit_reached` | more than 1000 apps per account |
| 502 | `upstream_unavailable` | API temporarily unavailable — retry later |

## Billing

Unchanged: active daemons are metered nightly (1 € per daemon per month),
no matter whether they were created in the panel or via this API.
