The 20-minute version of what an integrator (an intake app, a CI job, a
sibling service) needs to call fleet, written down because every piece of it
used to be discoverable only by reading deploy/Caddyfile, web/src/proxy.ts
and internal/sched/handlers/middleware.go. The task API itself is toured in
BUILDING-ON-FLEET.md and specified in
openapi.yaml; this page is about getting a request to it
at all.
fleet's Go listeners bind loopback: chat on 127.0.0.1:8080, the orchestrator
(the task API) on 127.0.0.1:8000. On the single-box install nothing else is
on :443 but Caddy, and Caddy routes by path
(ADR-0053):
https://<domain> path |
goes to | note |
|---|---|---|
/v1, /v1/* |
orchestrator | the API base — use this |
/api-info |
orchestrator | unauthenticated version discovery |
/.well-known/agent-card.json, /a2a, /a2a/* |
orchestrator | A2A (A2A.md) |
/triggers/* |
orchestrator | webhook + email task triggers |
/webhooks/* |
chat | GitHub/Slack-signed chat webhooks |
| everything else | Next.js web tier | pages + Next's own /api/* proxy |
Two consequences integrators hit first:
- Bare paths are not the API.
https://<domain>/tasksreaches the web app, which redirects a cookieless request to/login(a307); the same request under/v1/tasksreaches the orchestrator. Every route inopenapi.yamlis relative to/v1. - A box provisioned before the API routes shipped has none of them —
/v1/*also lands on the web app and 307s/404s.sudo fleet doctordetects that Caddyfile, rewrites it (backup kept) and then proveshttps://<domain>/api-infoanswers through Caddy;sudo fleet updateoffers the same rewrite. Nothing to hand-edit.
Quick reachability check, no credentials needed:
curl -sS https://fleet.example.com/api-info
# {"api_version":"1","fleet_version":"…","supported_versions":["1"],…}
# HTML or a redirect here = the proxy is not routing the API (run: sudo fleet doctor)sudo fleet sched apikey create manifest-dev --type task
# key store: /var/lib/fleet/data/api_keys.json (the fleet.service store …)
# created API key manifest-dev (id=key_… type=task)
# secret (shown once): fleet_task_…
# format: fleet_task_<base58> — send it as the X-API-Key header; …What to know:
- Key shapes. Typed keys are
fleet_<type>_<base58>:fleet_task_…creates and reads its own tasks,fleet_readonly_…only reads,fleet_webhook_…only fires its named triggers,fleet_admin_…is everything. Legacy--rolekeys aresk-<base64>.--helpand the create output both say this. - The store. The service reads
api_keys.jsonunder its data dir —FLEET_DATA_DIR/DATA_DIRfrom/etc/fleet/fleet.env, else./data, relative to the unit'sWorkingDirectory(/var/lib/fleet). The CLI now derives the same path, printskey store: …on every run, and warns whenFLEET_DATA_DIR/DATA_DIRin your shell points it somewhere else. A key minted into any other file is invisible to the service, and the caller sees a401indistinguishable from a typo. - Ownership. A root-run mint re-owned
api_keys.jsonto root inside thefleetuser's directory, so the service could neither read the new key nor save its own changes. The CLI now hands the store files back to the directory's owner after every write (chowned … to uid …when it had to). - The service picks a new key up without a restart (it re-reads the file on a lookup miss when the mtime moved).
Send the key as X-API-Key. That is the only public authentication path:
the proxy deletes the Next-proxy header-trust headers (X-User-Email,
X-User-Session-Epoch, the shared token headers) on every request it
forwards, so a cookie- or header-impersonation path does not exist from
outside the box.
FLEET=https://fleet.example.com/v1
KEY=fleet_task_…
curl -sS -X POST "$FLEET/tasks/estimate" -H "X-API-Key: $KEY" \
-H "Content-Type: application/json" -d '{"prompt":"ping","model":"anthropic/claude-opus-4-8"}'POST /v1/tasks/estimate is the recommended connection test: it runs the
same auth gate as a create, needs the same key scope, costs nothing and
creates nothing — a 200 proves reachability + key together; a 401 is a
key the service does not have (wrong store, wrong prefix, revoked); a 403
is a valid key of the wrong type for the route.
- Base URL
https://<domain>/v1; assertX-Fleet-API-Version: 1orGET /api-infoat startup (api-versioning.md). - Tasks:
POST /v1/tasks,GET /v1/tasks/{id},/stream,/output—BUILDING-ON-FLEET.md. - Outcome callbacks: task notification webhooks (NOTIFICATIONS.md, signing in WEBHOOK-SIGNING.md).
- Firing fleet from an external event:
POST /v1/triggers/{slug}with afleet_webhook_…key (EVENT-TRIGGERS.md).
MANIFEST_CALLBACK_TOKEN-style secrets and any other env var:printf '%s' "$V" | sudo fleet config set-env KEY— one line, deduped, file kept0600with its owner unchanged (ENV-CLI.md). Hand-editing is how a duplicate line took a deployment down: the server reads the last occurrence.FLEET_TRUSTED_PROXIES=127.0.0.1,::1so per-client rate limits and IP lists see the real caller behind Caddy (DEPLOYMENT.md).sudo fleet statusprobes the sandbox image in the service user's podman store (root's store is a different namespace; the old root-run report said "missing" for an image doctor had just verified).