Aetherfy agent REST API
Base URL and authentication for the Aetherfy control plane
The Aetherfy control plane is served from https://agents.aetherfy.com. Every
customer-facing route lives under /api/v1. Authentication is a bearer token:
Authorization: Bearer <api key>curl -s https://agents.aetherfy.com/api/v1/agents \
-H "Authorization: Bearer $AETHERFY_API_KEY"This is the same API key that authenticates the Aetherfy vector API and the same
one afy uses — see API keys. The key’s prefix selects the
environment: afy_live_ keys address your production account and afy_test_ keys
address the test environment. You never send an environment header; the credential
encodes it.
A request with no Authorization header is refused with 401 MISSING_API_KEY. A
key that does not resolve is refused with 401 INVALID_API_KEY — the same two
codes the Aetherfy vector API answers with.
The Aetherfy control plane is a different API from the vector API
Aetherfy runs two independent HTTP surfaces, and a client written against one will be subtly wrong against the other. The differences are not cosmetic.
Vector API (vectors.aetherfy.com) | Control plane (agents.aetherfy.com) | |
|---|---|---|
| What it does | Collections, points, search | Agents, deployments, runs, secrets, workspaces |
| Error envelope | {"error": {"code", "message"}} | {"detail": {"code", "message"}} |
| No credentials | 401 MISSING_API_KEY | 401 MISSING_API_KEY |
| Bad credentials | 401 INVALID_API_KEY | 401 INVALID_API_KEY |
| Suspended account | 403 ACCOUNT_SUSPENDED | 403 ACCOUNT_SUSPENDED |
| Rate-limit window | Fixed 60-second bucket | Sliding 60-second window |
Retry-After on 429 | Not sent | Sent |
| Request budget | Vector API requests/min | Control-plane requests/min — a separate budget |
The envelope is the trap, not the code. The auth codes are identical across
both surfaces, so the string you match on is the same either way — but the field
it arrives in is not. A handler that reads error.code finds nothing on a
control-plane response, and one that reads detail.code finds nothing on a vector
response. Read the field that matches the host you called.
Historical note, for a client written before 2026-08-20: the control plane used to
answer these three conditions with AUTH_REQUIRED, AUTH_INVALID_API_KEY and
AUTH_ACCOUNT_SUSPENDED. Those strings are retired and nothing returns them now.
The Aetherfy control-plane error envelope
Every error carries a stable code inside a detail object:
{
"detail": {
"code": "AGENT_NOT_FOUND",
"message": "Agent 'reporter' not found",
"agent": "reporter"
}
}Codes are append-only, so switching on detail.code is safe. The auth rename noted
above is the single exception on record, and it is closed. Individual errors add
context fields beside message —
agent, field, allowed, pending_deployments, dependents, current_state
and others — documented with the endpoint that emits them.
A DIFFERENT ENVELOPE COMES FROM YOUR AGENT’S OWN ADDRESS. Everything above
describes agents.aetherfy.com, the control plane. Requests you send to an
agent’s own URL do not go through it — they are answered by the edge in front
of *.aetherfy.dev, which uses {"error": {"code", "message"}} rather than
detail.code, and returns an aetherfy-request-id header on every response.
Three codes are its own:
| Code | Status | Means |
|---|---|---|
AGENT_NOT_FOUND | 404 | The hostname is not an agent address. Nothing was contacted. |
AGENT_NOT_REACHABLE | 404 | A well-formed agent address where nothing identified itself as a running agent. |
AGENT_UPSTREAM_FAILURE | 502 or 504 | A gateway answered before the agent did, so no agent response was produced. |
AGENT_NOT_FOUND is spelled the same here and on the control plane and means
the same thing; only the envelope differs. AGENT_NOT_REACHABLE has no control
plane equivalent — the control plane knows what an agent SHOULD be doing, and
the edge only knows what answered. When they disagree, the control plane is
authoritative: run afy agents list rather than inferring state from a request
that failed.
AGENT_UPSTREAM_FAILURE keeps the gateway’s own status — 502 or 504 — rather
than reporting a 404 like the two above. That difference is the useful part:
the status is the retry signal, and this condition is usually transient, while
a 404 would tell you never to retry and would also claim the address resolves
to nothing. Your agent may be perfectly healthy; something in front of it
answered first.
It names no cause, deliberately. A request body large enough to be refused
produces this, and so does an agent that stopped responding — the edge sees
the same thing in both cases and will not guess between them. Retry; if it
persists, afy agents logs shows what your agent did with the request.
The envelope covers routed errors only. A request to a path that matches no
route never reaches Aetherfy’s error handling, so it gets the framework default —
{"detail": "Not Found"} with a plain string, not an object — and
detail.code is undefined. If you are reading a code off a 404, check that
you called a path that exists before concluding the code is missing. The same gap
opens at the other end of the request: the control plane installs error handlers
for request validation and for database lock timeouts only, so a server error
neither covers falls through to the framework’s fallback, which answers 500
with the plain text Internal Server Error — not JSON at all, so parsing the
body throws before you can look for a code. A 500 that does carry one, such as
AUTHENTICATION_ERROR below, was raised by a route and kept the envelope.
Request-validation errors keep the envelope. When a request body fails schema
validation before reaching the handler, Aetherfy wraps the framework’s per-field
errors rather than passing them through, so detail is still an object with a
code — VALIDATION_ERROR — and the field-level detail moves into a
violations array beside message:
{
"detail": {
"code": "VALIDATION_ERROR",
"message": "Extra inputs are not permitted",
"violations": [
{"type": "extra_forbidden", "loc": ["body", "memorymb"],
"msg": "Extra inputs are not permitted", "input": 512}
]
}
}So detail.code is safe to read on a 422 like any other error, and field-level
handling reads detail.violations. Each violation carries the framework’s own
loc / msg / type, verbatim.
Errors every Aetherfy control-plane endpoint can return
These come from middleware and apply to every authenticated route below, so they are stated once here rather than repeated on each endpoint.
| Code | HTTP | Meaning |
|---|---|---|
MISSING_API_KEY | 401 | No Authorization header. Response carries WWW-Authenticate: Bearer |
INVALID_API_KEY | 401 | The key does not resolve — it may have been revoked |
ACCOUNT_SUSPENDED | 403 | The account is suspended for unpaid bills. Settle it in billing settings |
AUTH_SUBSCRIPTION_INACTIVE | 403 | The subscription is not active, trialing or past due. Carries subscription_status. Control-plane only — the vector API does not gate on subscription status, which is why this one keeps the AUTH_ prefix |
AUTHENTICATION_ERROR | 500 | Authentication itself failed. Retry |
RATE_LIMIT_EXCEEDED | 429 | The control-plane per-minute budget is exhausted — see below |
VALIDATION_ERROR | 422 | The request body did not match the schema. Field-level errors are in detail.violations |
RESOURCE_BUSY | 503 | A background worker held a row lock too long. Transient — retry in a few seconds |
SERVICE_UNAVAILABLE | 503 | A service this request depended on was unreachable, so Aetherfy could not answer either way. Transient — retry in a few seconds |
RESOURCE_BUSY is worth handling explicitly. Aetherfy bounds how long any request
may wait on a row a worker is holding, and answers 503 rather than hanging. It is
always safe to retry.
SERVICE_UNAVAILABLE is the other transient 503, and the two are worth telling
apart when you read logs: RESOURCE_BUSY is Aetherfy’s own database contending on
a row, while SERVICE_UNAVAILABLE means a dependency was down. Today it reaches
you from any route that reads your plan before acting — creating an agent,
deploying, spawning, placing a collection or workspace — because a plan that
cannot be read is a limit that cannot be checked, and Aetherfy refuses rather than
guessing. Nothing is written when it fires. The vector API at vectors.aetherfy.com
answers its own dependency outages with this same code and status, in error.code
rather than detail.code.
Rate limiting on the Aetherfy control plane
The control plane has its own per-minute request budget, separate from the vector API’s — saturating one leaves the other untouched. Per-plan numbers are in Limits.
Aetherfy returns X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset on successful responses, so you can pace without provoking a
429. When you do exceed it, the 429 tells you how long to wait:
{
"detail": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Limit: 500 requests/minute. Try again in 23 seconds.",
"limit": 500,
"retry_after_seconds": 23
}
}with Retry-After: 23 in the headers. Enterprise accounts are unlimited and receive
no X-RateLimit-* headers at all, so treat the headers as optional.
GET /api/v1/health is exempt from rate limiting.
Addressing an Aetherfy agent by name or by id
Every route spelled /agents/{agent} below accepts either the agent’s UUID or
its name. These are equivalent:
curl -s https://agents.aetherfy.com/api/v1/agents/reporter \
-H "Authorization: Bearer $AETHERFY_API_KEY"
curl -s https://agents.aetherfy.com/api/v1/agents/6f1c2b7e-0a2d-4f8e-9c31-2b0d5a7e4411 \
-H "Authorization: Bearer $AETHERFY_API_KEY"Names are unique per account, so there is no ambiguity. A name that does not resolve
returns 404 AGENT_NOT_FOUND — the same answer as an unknown UUID, and the same
answer as an agent belonging to another account. Aetherfy does not distinguish “does
not exist” from “not yours”.
Asynchronous responses in the Aetherfy control plane
Anything that touches machines is accepted and completed in the background. HTTP 202 means the request was accepted, not that the work finished.
| Response | What it means |
|---|---|
| 200 | Done. The body is the result |
| 201 | Created. The body is the new resource |
| 202 | Accepted. Work continues in the background — poll for completion |
| 204 | Done, no body |
The 202 bodies carry a progress label, not a status. POST /agents/{agent}/stop
answers {"status": "paused", …} and the agent’s own status field becomes
paused; archive answers "archiving" and restore answers "restoring", which
are not members of the status enum. Poll
GET /agents/{agent} for real state, or
GET /operations/{operation_id} for region changes.
Route index for the Aetherfy control plane
Fifty endpoints, grouped by the page that documents them.
Agent lifecycle
| Method | Path |
|---|---|
| POST | /api/v1/agents |
| GET | /api/v1/agents |
| GET | /api/v1/agents/{agent} |
| PATCH | /api/v1/agents/{agent} |
| DELETE | /api/v1/agents/{agent} |
| POST | /api/v1/agents/{agent}/stop |
| POST | /api/v1/agents/{agent}/start |
| POST | /api/v1/agents/{agent}/archive |
| POST | /api/v1/agents/{agent}/restore |
| GET | /api/v1/agents/{agent}/status |
| GET | /api/v1/agents/{agent}/yaml |
Deployments
| Method | Path |
|---|---|
| POST | /api/v1/agents/{agent}/deploy |
| GET | /api/v1/deployments |
| GET | /api/v1/agents/{agent}/deployments |
| GET | /api/v1/deployments/{deployment_id} |
| POST | /api/v1/agents/{agent}/deployments/{version}/rollback |
| POST | /api/v1/agents/{agent}/deployments/{version}/cancel |
| GET | /api/v1/deployments/{deployment_id}/payload |
| POST | /api/v1/agents/{agent}/github |
| GET | /api/v1/agents/{agent}/github |
| DELETE | /api/v1/agents/{agent}/github |
Runs, schedules and spawns
| Method | Path |
|---|---|
| POST | /api/v1/agents/{agent}/run |
| GET | /api/v1/agents/{agent}/runs |
| POST | /api/v1/agents/{agent}/schedule/pause |
| POST | /api/v1/agents/{agent}/schedule/resume |
| POST | /api/v1/agents/{agent}/spawn |
| GET | /api/v1/agents/{agent}/logs |
Secrets
| Method | Path |
|---|---|
| POST | /api/v1/agents/{agent}/secrets |
| GET | /api/v1/agents/{agent}/secrets |
| DELETE | /api/v1/agents/{agent}/secrets/{key} |
| POST | /api/v1/agents/{agent}/secrets/{key}/rotate |
| POST | /api/v1/workspaces/{workspace}/secrets |
| GET | /api/v1/workspaces/{workspace}/secrets |
| DELETE | /api/v1/workspaces/{workspace}/secrets/{key} |
Workspaces, regions and operations
| Method | Path |
|---|---|
| POST | /api/v1/workspaces |
| GET | /api/v1/workspaces |
| GET | /api/v1/workspaces/{workspace} |
| PATCH | /api/v1/workspaces/{workspace} |
| DELETE | /api/v1/workspaces/{workspace} |
| GET | /api/v1/workspaces/{workspace}/agents |
| PATCH | /api/v1/workspaces/{workspace}/regions |
| PATCH | /api/v1/collections/{collection_id} |
| GET | /api/v1/operations/{operation_id} |
Account and usage
| Method | Path |
|---|---|
| GET | /api/v1/auth/me |
| PATCH | /api/v1/users/me/home-region |
| GET | /api/v1/usage/summary |
| GET | /api/v1/usage/agent/{agent_id}/events |
| GET | /api/v1/auth/github |
| GET | /api/v1/auth/github/status |
| DELETE | /api/v1/auth/github |
Aetherfy control-plane routes that are not customer API
Two groups of routes exist on agents.aetherfy.com and are deliberately absent from
the index above.
Everything under /internal/ is a private contract between the Aetherfy dashboard
and the control plane, authenticated by a shared secret rather than by your API key.
Your key will not open it. POST /api/v1/webhooks/github and
POST /api/v1/webhooks/github/{agent_id} are called by GitHub and authenticated by
webhook signature, not by a bearer token.
The health probes — GET /api/v1/health, /readiness and /liveness — are
unauthenticated and exist for Aetherfy’s own monitoring. They are not part of the
customer API and carry no compatibility promise.
There is no build-log endpoint. Read build progress from
GET /api/v1/deployments/{deployment_id}, whose state
and error_message are real. An agent’s runtime logs are a different thing and do
have a route — GET /agents/{agent}/logs.
Machine-readable index of the Aetherfy control plane
The routes on this page are also published as OpenAPI 3.1 at https://docs.aetherfy.com/agents-openapi.json. It is a second document, separate from openapi.json, which describes the vector API — different origin, different auth budget, different error envelope. See Aetherfy for AI agents.
The Aetherfy CLI does most of this for you
Every route here is reachable through afy, which handles archiving, auth and
polling. The REST API is the right tool when you are building automation that cannot
shell out, or a client in a language with no Aetherfy SDK. Start at
the CLI reference if you are not.