> For the complete documentation index, see [llms.txt](https://fleet.hackmap.win/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fleet.hackmap.win/reference/rest-api.md).

# REST API

All paths are under `/api/v1`. All responses are JSON. All errors return the FastAPI default `{"detail": "..."}` shape.

## Auth

Three checks, one bearer-token style:

* **Admin endpoints** — `Authorization: Bearer $FLEET_ADMIN_TOKEN`. Full access.
* **Scoped endpoints** — `Authorization: Bearer <some-token-from-FLEET_TOKEN_SCOPES>`. Per-automation access only. The admin token implicitly has `*` (every type). See [auth](/operations/auth.md).
* **Worker endpoints** — `Authorization: Bearer $FLEET_WORKER_TOKEN` (or any token listed in `FLEET_WORKER_TOKENS` during rotation). The WS endpoint also accepts the token via `?token=` for environments that can't set headers.

`401` for missing/malformed `Authorization`; `403` for valid Bearer but insufficient privilege.

## Discovery

### `GET /api/v1/automations`

Lists every automation type the master knows about. Admin auth.

```json
[
  {
    "type": "url-pinger",
    "class": "UrlPinger",
    "kind": "ContinuousAutomation",
    "config_schema": { "type": "object", "properties": {...} }
  }
]
```

The `config_schema` is the JSON Schema from the plugin's Pydantic `Config` class. The dashboard uses this to render edit forms.

## Catalog

The catalog exposes the full Pydantic schemas (config + output + task payload + pool tags/payload) for every registered automation. Use it to look up wire shapes when you don't have access to the plugin's Python module — e.g., from a UI, an external scheduler, or another language.

### `GET /api/v1/catalog`

Every automation's full schema bundle. Admin auth.

```json
{
  "cf-cookie-harvester": {
    "type": "cf-cookie-harvester",
    "kind": "BatchAutomation",
    "config":  {"type": "object", "properties": {...}},
    "queue":   {"name": "cf-cookie-harvester", "payload": {...TaskPayload schema...}},
    "stream":  {"name": "cf-cookie-harvester", "payload": {...Output schema...}},
    "pools":   {"cookies": {"payload": {...}, "tags": {...}}}
  }
}
```

`queue`, `stream`, `pools` are present only when the automation declared them.

### `GET /api/v1/catalog/{automation_type}`

One automation's catalog entry. 404 if unknown. Scoped auth.

See [contracts](/concepts/contracts.md) for what the schemas mean and how they're declared.

## Workers

### `GET /api/v1/automations/{type}/workers`

Every worker registered under `type`, with its current state. Scoped auth.

```json
[
  {
    "worker_id": "host1",
    "automation_type": "url-pinger",
    "hardware": {"cores": 4, "ram_gb": 8, "hostname": "host1", "kernel": "..."},
    "config": {...},
    "config_gen": 11,
    "state": "RUNNING",
    "last_seen": 1778690712.95,
    "last_error": null,
    "current_stats": {
      "slots": 4, "cpu_pct": 15.2, "rss_mb": 320, "ts": ...,
      "metrics": {...},
      "per_slot": [
        {"slot_id": 0, "age_seconds": 312.4, "counters": {"ok": 42}},
        {"slot_id": 1, "age_seconds": 18.0,  "counters": {"ok": 3}}
      ]
    }
  }
]
```

`per_slot` is the new fan-out from feature 4.3 — every slot reports its own uptime + counter snapshot so you can spot the one bad slot in a fleet.

### `GET /api/v1/automations/{type}/workers/{id}`

One worker. Same shape as above. 404 if not registered. Scoped auth.

### `PATCH /api/v1/automations/{type}/workers/{id}/config`

Merge the request body into the worker's config, validate against the plugin's `Config` schema, bump `config_gen`, push a WebSocket `config_changed` frame to the worker. Scoped auth.

```bash
curl -X PATCH http://master/api/v1/automations/url-pinger/workers/host1/config \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "slots": 4, "url": "https://example.com"}'
```

Response: `{"config": {...merged...}, "config_gen": 12}`. 422 on schema validation failure. The merge is shallow — top-level fields only.

### `GET /api/v1/automations/{type}/workers/{id}/config`

Worker-facing poll endpoint. Worker auth. Returns `{"config": {...}, "config_gen": N}`. The worker hits this every 30 seconds as a safety net for missed WS `ConfigChanged` frames.

### `GET /api/v1/automations/{type}/workers/{id}/stats?since=&limit=`

Time-series of `Stats` frames previously emitted by this worker. Scoped auth.

```bash
curl "http://master/api/v1/automations/url-pinger/workers/host1/stats?since=1778600000&limit=120" \
  -H "Authorization: Bearer $TOKEN"
```

Returns up to `limit` frames (default 240) with `ts >= since`. Each frame is the full `Stats` shape (cpu/rss/slots/metrics/per\_slot). Backed by a ring buffer trimmed to 240 entries per worker by default — older frames are dropped on push. See feature 4.4.

### `POST /api/v1/automations/{type}/workers/{id}/drain`

Tell a worker to gracefully drain: stop accepting new tasks/slots and finish what's in flight before exiting. Scoped auth.

Returns `{"worker_id": "...", "drain": "requested"}`. The worker reports `state="DRAINING"` via Stats until it disconnects.

### `DELETE /api/v1/automations/{type}/workers/{id}`

Forget a worker that's no longer connecting (e.g. its host is gone). Scoped auth. 404 if unknown. Returns `{"worker_id": "...", "removed": true}`.

This only clears the master's view; if the worker is still alive it will re-register on the next WS reconnect.

## Streams

### `GET /api/v1/automations/{type}/output/length`

Stream length. Scoped auth. `{"length": 142}`.

### `GET /api/v1/automations/{type}/output/peek?n=10`

Up to `n` oldest entries without removing them. Scoped auth.

```json
[
  {"id": "abc...", "slot_id": 0, "ts": 1778..., "payload": {...}},
  ...
]
```

### `POST /api/v1/automations/{type}/output/pop`

Atomically remove and return up to `n` oldest entries. Scoped auth.

```bash
curl -X POST http://master/api/v1/automations/url-pinger/output/pop \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"n": 5}'
```

No long-polling. If the stream is empty you get an empty list immediately.

## Task submission (BatchAutomation)

### `POST /api/v1/automations/{type}/tasks`

Submit one task to a `BatchAutomation`'s queue. The master validates the payload against the automation's declared `TaskPayload` schema before queueing — bad payloads fail fast with 422 instead of crashing a worker mid-task. Scoped auth; subject to `FLEET_SUBMIT_RPS_PER_TOKEN` rate limit.

```bash
curl -X POST http://master/api/v1/automations/cf-cookie-harvester/tasks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {"site": "example.com"},
    "priority": 1,
    "ttl_seconds": 60,
    "max_attempts": 5,
    "idempotency_key": "harvest-example.com-2026-05-15"
  }'
```

Request body fields:

| field             | type              | purpose                                                                                                                                        |
| ----------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `payload`         | object (required) | validated against the automation's `TaskPayload`                                                                                               |
| `task_id`         | string            | omit to get a uuid hex                                                                                                                         |
| `priority`        | int (default 0)   | `>0` routes to high tier (drained first), `<0` to low (drained last)                                                                           |
| `ttl_seconds`     | int               | task is silently dropped on reserve when older than this. Use for "act now or never" events                                                    |
| `max_attempts`    | int               | override `Config.max_attempts` per task. After N failures the task is DLQ'd                                                                    |
| `idempotency_key` | string            | second submit with the same (type, key) inside `FLEET_IDEMPOTENCY_TTL_SECONDS` (default 24h) returns the original `task_id` without enqueueing |

Response:

```json
{"task_id": "abc123...", "queued": true, "priority": 0}
```

Duplicate (idempotent re-submit):

```json
{"task_id": "abc123...", "queued": false, "duplicate": true}
```

Errors:

* **400** — the automation type isn't a `BatchAutomation` (no `TaskPayload` declared).
* **404** — unknown automation type.
* **422** — payload failed schema validation. The Pydantic error is in `detail`.
* **429** — rate limit exceeded for this token.

## Queue inspection

### `GET /api/v1/automations/{type}/queue?n=20`

Pending tasks (does **not** consume them). Scoped auth.

```json
{
  "length": 17,
  "items": [
    {"id": "abc", "payload": {...}, "attempt": 0, "submitted_at": 1778...},
    ...
  ]
}
```

Items are returned in priority order (high → normal → low) and within each tier in FIFO order. Use this to see what's queued and why — the dashboard's queue card hits this endpoint.

## Dead-letter queue

### `GET /api/v1/automations/{type}/dead?n=50`

Peek the DLQ. Scoped auth.

```json
{
  "length": 3,
  "items": [
    {"id": "abc", "payload": {...}, "attempt": 5, "submitted_at": ..., "max_attempts": 5},
    ...
  ]
}
```

### `POST /api/v1/automations/{type}/dead/replay`

Re-enqueue up to `n` DLQ items at the back of the normal queue with `attempt` reset to 0. Scoped auth.

```bash
curl -X POST .../dead/replay -d '{"n": 10}' ...
```

Returns `{"replayed": 10}`.

### `DELETE /api/v1/automations/{type}/dead`

Drop every DLQ item. Scoped auth. Returns `{"cleared": N}`. Irreversible.

## Pool inspection

### `GET /api/v1/pools/{pool_name}?n=20`

Items currently in a pool, with their tags + payload + expires\_at + in\_use\_until. Admin auth (pool names are global, not per-automation).

```json
{
  "size": 12,
  "items": [
    {
      "id": "i-abc",
      "tags": {"site": "example.com", "fresh": true},
      "payload": {"cookies": {...}, "user_agent": "..."},
      "expires_at": 1778693100.0,
      "in_use_until": null
    }
  ]
}
```

`in_use_until` is non-null while a slot has the item claimed. Use this to debug "items are present but nobody's getting them" — every entry stuck `in_use_until > now` is a slot crash that didn't release.

## Metrics

### `GET /metrics`

Prometheus text-exposition format. No auth in-process (intended for scrape over a private network — block the path at your reverse proxy if it must traverse public infrastructure). See [observability](/operations/observability.md) for the full metric list.

## Health

### `GET /healthz`

No auth. Returns `{"status": "ok"}` when the master is up. Suitable for load balancer health checks.

## Dashboard

`GET /` serves the operator dashboard (HTML + JS). Requires the admin token, prompted via `localStorage` on first load. See [dashboard guide](/guides/dashboard.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://fleet.hackmap.win/reference/rest-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
