Aetherfy workspaces and regions API
Base URL https://agents.aetherfy.com/api/v1, bearer authentication, and the shared
error envelope are described on the Agent REST API index.
A workspace groups agents and collections and gives them a shared region set and shared secrets. For the concept, see Workspaces.
Creating an Aetherfy workspace
POST /api/v1/workspaces → 201
curl -s -X POST https://agents.aetherfy.com/api/v1/workspaces \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"research","description":"Research agents","regions":["iad","fra"]}'| Field | Type | Notes |
|---|---|---|
name | string | Required. 3–63 characters, unique per account |
description | string | null | Optional |
regions | string[] | null | Optional. Omit to inherit your account’s default placement |
Omitting regions is the normal case: Aetherfy resolves the workspace to your
account’s default placement rather than leaving it unset. Supplying an empty array
is a different thing and is rejected — a workspace with no regions could hold nothing.
The region set must fit your plan’s maximum. On Free and Starter that maximum is one, so a multi-region workspace requires the Performance plan — see Regions and replication.
{
"id": "d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3",
"name": "research",
"description": "Research agents",
"regions": ["iad", "fra"],
"agent_count": 0,
"created_at": "2026-08-19T09:30:00Z",
"updated_at": "2026-08-19T09:30:00Z"
}| Code | HTTP | Meaning |
|---|---|---|
WORKSPACE_NAME_TAKEN | 409 | |
WORKSPACE_LIMIT_EXCEEDED | 400 | Your plan’s workspace count is reached |
WORKSPACE_REGIONS_EMPTY | 422 | regions was an empty array. Omit it instead |
INVALID_WORKSPACE_REGIONS | 400 | A region code is unknown, or there are more than your plan allows |
USER_NOT_FOUND | 404 |
Reading and updating an Aetherfy workspace
GET /api/v1/workspaces → 200, an array.
GET /api/v1/workspaces/{workspace} → 200, one workspace.
PATCH /api/v1/workspaces/{workspace} → 200.
The path parameter is the workspace name, not its UUID.
curl -s -X PATCH https://agents.aetherfy.com/api/v1/workspaces/research \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description":"Research agents, 2026"}'PATCH changes description only in practice. A workspace name is immutable: you
may send name, but it has to equal the name already in the URL, and anything else is
refused with 400 WORKSPACE_NAME_IMMUTABLE. To rename, create a new workspace and
move resources into it.
Regions are not changed here either — they have their own route below.
Listing the agents in an Aetherfy workspace
GET /api/v1/workspaces/{workspace}/agents → 200
[
{
"id": "6f1c2b7e-0a2d-4f8e-9c31-2b0d5a7e4411",
"name": "reporter",
"agent_type": "job",
"status": "running",
"deployed": true,
"url": null,
"is_degraded": false,
"regions_total": 2,
"regions_ready": 2,
"degraded_reason": null,
"created_at": "2026-08-19T09:04:00Z",
"updated_at": "2026-08-19T09:06:02Z"
},
{
"id": "9d3f8a1c-77b2-4e60-8a15-c4e9f2d61b03",
"name": "api",
"agent_type": "service",
"status": "running",
"deployed": true,
"url": "https://api-m4t7q3.aetherfy.dev",
"is_degraded": false,
"regions_total": 2,
"regions_ready": 2,
"degraded_reason": null,
"created_at": "2026-08-19T09:04:00Z",
"updated_at": "2026-08-19T09:06:02Z"
}
]Both agents are deployed; only one has a url. A service listens for
requests and therefore has an address. A job runs to completion and exits —
its image is not a web server, so there is nowhere to send a request and url
is null. Read deployed to ask whether an agent is up; read url to ask
where to reach it. They are different questions and were once one field.
A summary shape, not the full agent object — read
GET /agents/{agent} for that.
deployed and url answer two different questions, and the example above is
the case that shows why. deployed is whether the agent holds a running app —
it is what the plan’s agent quota counts, and what archive, pause and run-now
are offered on. url is where to send a request, and this agent has none: a
job runs once and exits behind no HTTP server. Read deployed to tell whether
an agent is running; read url only when you want to call one.
serves_websocket is a third read-only field, and it is derived rather than
declared: Aetherfy reads what your deployed app actually serves — the live
route table on the Python runtimes, the live upgrade handlers on node and bun
— and reports it back. You never set it.
It has three values, and the third is the one worth knowing. true and
false mean the agent answered and does or does not serve WebSocket routes.
null means Aetherfy has no usable answer yet, which is not the same as
false — a newly created agent, or one that has been asleep since it
deployed, has told us nothing. Branch on === true rather than on
truthiness if the difference matters to you.
Note that it says what an agent ADDITIONALLY accepts. Every service agent
serves HTTP, because Aetherfy health-checks GET /health on all of them, so
serves_websocket: true means HTTP and WebSocket rather than one instead of
the other.
Deleting an Aetherfy workspace
DELETE /api/v1/workspaces/{workspace} → 200
{"status": "deleted", "secrets_deleted": 3}Deletion is refused while the workspace still holds anything, and the response tells you which kind of thing is blocking it. Move or delete those first.
secrets_deleted reports how many workspace secrets went with it — they are the one
thing deletion does remove, since nothing else can reach them.
| Code | HTTP | Meaning |
|---|---|---|
WORKSPACE_HAS_AGENTS | 409 | Move or delete its agents first |
WORKSPACE_HAS_COLLECTIONS | 409 | Move or delete its collections first |
Changing an Aetherfy workspace’s regions
PATCH /api/v1/workspaces/{workspace}/regions → 202
curl -s -X PATCH https://agents.aetherfy.com/api/v1/workspaces/research/regions \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"regions":["iad","fra","sin"]}'regions is the desired final set, not a delta. Aetherfy computes what to add and
remove.
{
"operation_id": "f2a7c390-4e81-4b25-9d06-8c1f5e0b7a43",
"from_regions": ["iad", "fra"],
"to_regions": ["iad", "fra", "sin"],
"regions_to_add": ["sin"],
"regions_to_remove": [],
"workspace_name": "research"
}This is asynchronous. The 202 hands you an operation_id to poll rather than blocking
until every collection has replicated — see below.
A no-op answers 200, not 202, and has no operation_id. If the set you send already
equals the current one, Aetherfy enqueues nothing and returns a different body:
{"status": "no_op", "scope": "workspace", "current_regions": ["iad", "fra"], "workspace_name": "research"}The comparison is by set, so reordering the same regions is also a no-op. Branch on the
status code — or on the presence of operation_id — before trying to poll, because
polling a no-op has nothing to poll.
The change affects the workspace’s collections and agents together. Widening the set makes a region available; getting a running agent into it still requires a redeploy.
Adding a region replicates the workspace’s collections into it, and Aetherfy counts
every replica against your storage quota — so a widen can be refused with
STORAGE_LIMIT_EXCEEDED for space rather than for placement. Check headroom before
widening a large workspace.
| Code | HTTP | Meaning |
|---|---|---|
WORKSPACE_NOT_FOUND | 404 | |
USER_NOT_FOUND | 404 | |
STORAGE_LIMIT_EXCEEDED | 422 | The new replicas would exceed your storage quota |
INVALID_WORKSPACE_REGIONS | 400 | Unknown region, or more than your plan allows |
REGION_OP_IN_FLIGHT | 409 | A region change is already running for this workspace |
WORKSPACE_NARROW_BLOCKED_BY_RESOURCES | 422 | Narrowing would strand resources that still use a region you are removing |
WORKSPACE_NARROW_BLOCKED_BY_RESOURCES is the narrow counterpart to
STORAGE_LIMIT_EXCEEDED: it refuses the change rather than shrinking anything, so a
narrow never deletes data as a side effect. Its body names every blocker at once —
regions_to_remove, plus an agent_conflicts array and a collection_conflicts
array, each entry giving the resource name and the regions it currently holds:
{"detail": {"code": "WORKSPACE_NARROW_BLOCKED_BY_RESOURCES",
"message": "This region change removes ['sin'], but 2 resource(s) still use a region being removed. ...",
"regions_to_remove": ["sin"],
"agent_conflicts": [{"name": "pdf-parser", "deployment_regions": ["iad", "sin"]}],
"collection_conflicts": [{"name": "documents", "regions": ["iad", "sin"]}]}}Clear each conflict first — redeploy the listed agents without the region, and PATCH
each listed collection to drop it — then retry the narrow. Note this one is a
top-level detail.code, not a pre-flight violation entry: it is checked after the
validations below pass, so you will not see it in a detail.violations array.
Pre-flight violations on an Aetherfy region change
Before enqueueing, Aetherfy checks the whole change and reports every problem
at once rather than failing on the first. That answer is 400 with the top-level code
VALIDATION_VIOLATIONS and a detail.violations array — each entry carrying its own
code. Read the array, not the top-level code, or you will see one failure and fix
one of four.
Violation code | Meaning |
|---|---|
INVALID_REGION | Not a region Aetherfy runs in. One entry per bad region, each naming it |
PLAN_REGION_LIMIT | More regions than your plan allows for this scope. Names the limit and what you asked for |
REGION_UNAVAILABLE | The region is temporarily closed to new placements. Transient — retry later |
COMPONENT_NOT_READY | A collection or agent in the workspace is mid-transition. Names it and its state; wait for a stable one |
AGENT_REGION_STRANDED | An agent has a live deployment in a region this change would remove. Lists deployment_regions, target_regions and stranded_regions — redeploy the agent narrower, or widen the target set |
INVALID_REGION is the one code here that also appears at the top level, on
PATCH /users/me/home-region — same code, different position
in the envelope, because that route validates a single region rather than a set.
Moving an Aetherfy collection between workspaces
PATCH /api/v1/collections/{collection_id} → 200 or 202
curl -s -X PATCH https://agents.aetherfy.com/api/v1/collections/$COLLECTION_ID \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workspace_id":"d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3"}'| Field | Type | Notes |
|---|---|---|
workspace_id | uuid | null | Target workspace, or null to move the collection out of any workspace |
regions | string[] | null | An explicit region set for the collection |
The path parameter is the collection’s UUID, not its name — this is a control-plane route, unlike the name-addressed collection routes on the vector API.
Send workspace_id or regions, not both: combining them is refused, because the
workspace already implies a placement and the two could disagree. When the move needs
data to relocate, the answer is 202 with an operation_id; when nothing has to move,
it is 200.
Aliases do not survive the move. Aetherfy deletes the collection’s aliases in the
source scope and does not recreate them in the target. The completed operation lists
what went in deleted_aliases so you can recreate them deliberately.
| Code | HTTP | Meaning |
|---|---|---|
COLLECTION_NOT_FOUND | 404 | |
TARGET_WORKSPACE_NOT_FOUND | 404 | |
COLLECTION_MOVE_IN_FLIGHT | 409 | A move is already running for this collection |
COLLECTION_REGIONS_WORKSPACE_COMBINED | 400 | Both workspace_id and regions were sent |
COLLECTION_REGIONS_EMPTY | 422 | regions was an empty array |
COLLECTION_REGIONS_NOT_IN_SCOPE | 422 | The requested regions are outside the target workspace’s set |
TARGET_WORKSPACE_HAS_NO_REGIONS | 422 | The target workspace has no region set to inherit |
VALIDATION_VIOLATIONS | 400 | Several rules failed at once. Iterate detail.violations |
COLLECTION_MOVE_ENQUEUE_FAILED | 500 | The move could not be queued. Retry |
VALIDATION_VIOLATIONS is the accumulating envelope: rather than failing on the first
problem, Aetherfy reports every rule the request broke in one violations array, each
entry carrying its own code. Read that array rather than the top-level message.
Polling an Aetherfy long-running operation
GET /api/v1/operations/{operation_id} → 200
Both the region change and the collection move above return an operation_id. This is
where you follow it.
curl -s https://agents.aetherfy.com/api/v1/operations/$OPERATION_ID \
-H "Authorization: Bearer $AETHERFY_API_KEY"{
"operation_id": "f2a7c390-4e81-4b25-9d06-8c1f5e0b7a43",
"scope": "workspace",
"target_id": "d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3",
"status": "succeeded",
"regions_to_add": ["sin"],
"regions_to_remove": [],
"started_at": "2026-08-19T09:31:02Z",
"completed_at": "2026-08-19T09:34:47Z",
"failures": null,
"deleted_aliases": null
}status | Meaning |
|---|---|
pending | Queued, not started |
in_progress | Running, or briefly between retries |
succeeded | Terminal, done |
failed | Terminal. failures carries the reasons |
in_progress deliberately covers the short window after a retryable sub-step failed
and before it is retried, so a transient blip does not surface as failed. Only
failed is a terminal failure.
failures is an array of structured per-step failures when a saga fails, falling back
to a single message when there is no structured detail. deleted_aliases is populated
on a collection move and null on a region change.
Returns 404 OPERATION_NOT_FOUND for an unknown id or one belonging to another
account.