Aetherfy deployments API
Base URL https://agents.aetherfy.com/api/v1, bearer authentication, and the shared
error envelope are described on the Agent REST API index. {agent}
accepts an agent UUID or its name throughout.
Deploying an Aetherfy agent
POST /api/v1/agents/{agent}/deploy → 202
This is a multipart upload, not JSON. One part, code_archive, carrying a
gzipped tarball of your agent directory.
tar czf agent.tar.gz -C ./my-agent .
curl -s -X POST "https://agents.aetherfy.com/api/v1/agents/reporter/deploy" \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-F "code_archive=@agent.tar.gz"| Parameter | In | Notes |
|---|---|---|
code_archive | multipart form | Required. Gzipped tarball of the agent directory |
confirm_overage | query | Default false. Set true to accept a deploy that adds usage |
If aetherfy.yaml sits at the archive root, Aetherfy parses it and applies it to the
agent before building — runtime, memory, regions, schedule, spawn configuration. Your
uploaded configuration wins over what the agent record already held, so the archive
is the source of truth for a deployed agent. See
the aetherfy.yaml reference.
The 202 response is a deployment object (below). The build has been queued, not finished — poll the deployment to follow it.
Cost confirmation on an Aetherfy deploy
A deploy that would push the account into paid usage is answered 402
OVERAGE_CONFIRM_REQUIRED the first time, naming the additional monthly amount in
dollars. This is a disclosure, not a block: repeat the request with
?confirm_overage=true and it proceeds.
curl -s -X POST "https://agents.aetherfy.com/api/v1/agents/reporter/deploy?confirm_overage=true" \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-F "code_archive=@agent.tar.gz"Errors on an Aetherfy deploy
Aetherfy checks account-level gates before per-agent ones, so a frozen account is told that rather than being sent to delete an agent that would not unblock it.
| Code | HTTP | Meaning |
|---|---|---|
SOFT_CAP_EXCEEDED | 403 | A spend limit was reached. Existing agents keep running; new deploys are refused |
DUNNING_FROZEN | 403 | A payment failed and the account is in its grace period |
OVERAGE_CONFIRM_REQUIRED | 402 | Confirm the added cost — see above |
PLAN_LIMIT_EXCEEDED | 403 | Agent count, memory, regions or always-on exceeds the plan |
DEPLOY_REGIONS_NOT_IN_SCOPE | 403 | Requested regions are outside the workspace’s region set |
DEPLOY_REGIONS_NOT_IN_COLLECTION_SCOPE | 403 | The declared collection is not replicated to every deploy region |
INVALID_REGION | 400 | A regions: entry is not a region Aetherfy runs in. The body’s regions array names the bad ones |
AGENT_PAUSED_CANNOT_DEPLOY | 409 | Start the agent first |
DEPLOYMENT_IN_PROGRESS | 409 | A build is already running. Transient — retry |
DEPLOYMENT_ARCHIVE_TOO_LARGE | 413 | The upload exceeded the archive cap |
DEPLOYMENT_CONFIG_PARSE_ERROR | 422 | aetherfy.yaml is malformed or invalid |
RUNTIME_IMMUTABLE | 422 | An agent’s runtime cannot change after its first deploy |
AGENT_SCHEDULE_NOT_ALLOWED_ON_WORKER | 422 | A spawned worker cannot carry a schedule: |
COLLECTION_NOT_FOUND | 404 | database_collection does not resolve |
WORKER_NOT_FOUND | 404 | A name in spawn.workers does not resolve |
DEPLOYMENT_UPLOAD_FAILED | 500 | Aetherfy could not store the archive. Retry |
The agent-count limit is enforced here, not at
agent creation — creating an agent is free, because the
limit counts deployed agents and a new agent is a draft. So
PLAN_LIMIT_EXCEEDED on the first deploy of an agent that created cleanly is
expected behaviour and not a contradiction: the create never claimed a slot for
it. Either every slot was already taken when you created the draft, or one was
free then and is not now, because a downgrade landed or another agent took the
last one in between.
Reading Aetherfy deployments
GET /api/v1/deployments → 200, every deployment on the account, newest first.
Accepts ?agent_id=<uuid> to narrow.
GET /api/v1/agents/{agent}/deployments → 200, one agent’s deployments.
GET /api/v1/deployments/{deployment_id} → 200, one deployment.
curl -s https://agents.aetherfy.com/api/v1/agents/reporter/deployments \
-H "Authorization: Bearer $AETHERFY_API_KEY"{
"id": "0b7d1f66-3a9e-4c25-9f10-77c2b4e51a08",
"agent_id": "6f1c2b7e-0a2d-4f8e-9c31-2b0d5a7e4411",
"version": 7,
"state": "active",
"image_size_mb": 143,
"regions": ["iad", "fra"],
"pending_regions": [],
"regions_total": 2,
"regions_ready": 2,
"queue_position": null,
"is_serving": true,
"is_degraded": false,
"is_ephemeral": false,
"can_redeploy": true,
"can_rollback": true,
"cancellation_requested": false,
"cancellation_reason": null,
"pending_region_alert_stage": null,
"created_at": "2026-08-19T09:04:11Z",
"started_at": "2026-08-19T09:04:19Z",
"deployed_at": "2026-08-19T09:06:02Z",
"error_message": null
}Aetherfy deployment states
| State | Meaning |
|---|---|
queued | Waiting for a builder. queue_position is set |
building | Building the image |
deploying | Built; machines coming up region by region |
active | Serving |
failed | Terminal. Read error_message |
rolled_back | Superseded by a rollback to an earlier version |
superseded | A newer deployment replaced it |
completed | Terminal, and not a failure — an ephemeral run that finished |
completed only appears on ephemeral deployments: the one-shot runs produced by
a scheduled task, a manual run, or a spawn. A long-lived service
deployment never reaches it.
Watch is_serving rather than state alone when you need “is traffic being
answered”. A multi-region deployment can be deploying with some regions already
live, which is what regions_ready against regions_total tells you, and
is_degraded with pending_region_alert_stage says a region is taking too long.
| Code | HTTP | Meaning |
|---|---|---|
DEPLOYMENT_NOT_FOUND | 404 | Unknown id, or it belongs to another account |
DEPLOYMENT_ACCESS_DENIED | 403 | The deployment’s agent is not yours |
Rolling back an Aetherfy agent
POST /api/v1/agents/{agent}/deployments/{version}/rollback → 202
{version} is the integer version you want to return to, not a deployment id.
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/deployments/6/rollback \
-H "Authorization: Bearer $AETHERFY_API_KEY"Rollback re-deploys a previously built image, so it does not rebuild and does not need the original source. It creates a new version rather than reviving the old number. Only versions whose image still exists are valid targets; the rejection names that as the reason.
| Code | HTTP | Meaning |
|---|---|---|
DEPLOYMENT_ROLLBACK_TARGET_INVALID | 422 | That version is not a rollback target — most often its image is no longer available. can_rollback on the deployment object answers this before you call |
DEPLOYMENT_NOT_FOUND | 404 | No such version for this agent |
DEPLOYMENT_IN_PROGRESS | 409 | A build is running. Transient — retry |
AGENT_PAUSED_CANNOT_ROLLBACK | 409 | Start the agent first |
SOFT_CAP_EXCEEDED / DUNNING_FROZEN | 403 | The account is frozen |
See Rollback for the operational guide.
Redeploying an Aetherfy agent
POST /api/v1/agents/{agent}/deployments/{version}/redeploy → 202
{version} is the integer version whose source you want rebuilt, not a deployment id.
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/deployments/6/redeploy \
-H "Authorization: Bearer $AETHERFY_API_KEY"Redeploy runs a fresh build from that version’s stored code archive, which is what separates it from rollback: rollback re-deploys an image that already exists, so it cannot pick up a secret written since. Secrets are injected while the machine is built, so redeploy is how a stored secret reaches a running service. It creates a new version.
The agent keeps its current configuration — the archive is not re-read for memory, regions or any other setting, so redeploying an old version never reverts a change you made after it shipped.
Archives are not kept forever: Aetherfy stores them for the three most recent successful
deployments, and deletes one when its build fails. can_redeploy on the deployment
object tells you whether a given version still has its source; read it rather than
inferring from the state.
This applies to GitHub-deployed versions too: a version whose archive has aged out is
not redeployable even though the commit is still in your repository. Aetherfy does not
re-clone the commit, because re-reading its aetherfy.yaml would revert configuration
you changed afterwards. Push again instead — a push deploys with current secrets.
Redeploy is not a retry for a failed build. A build that fails has its archive
deleted immediately, so there is nothing left to rebuild — and re-running an identical
build would fail identically. To recover from a failed build, deploy your code again
with afy deploy, or push to the linked repository. What redeploy is for is taking a
deployment that built successfully and running its build again against current secrets.
The one failure it does cover is a deployment that built but failed to launch, which
keeps its archive.
| Code | HTTP | Meaning |
|---|---|---|
DEPLOYMENT_REDEPLOY_SOURCE_UNAVAILABLE | 422 | That version’s stored code archive is gone, so it cannot be rebuilt |
DEPLOYMENT_NOT_FOUND | 404 | No such version for this agent |
DEPLOYMENT_IN_PROGRESS | 409 | A build is running. Transient — retry |
AGENT_PAUSED_CANNOT_REDEPLOY | 409 | Start the agent first |
SOFT_CAP_EXCEEDED / DUNNING_FROZEN | 403 | The account is frozen |
Cancelling an in-flight Aetherfy deployment
POST /api/v1/agents/{agent}/deployments/{version}/cancel → 202, returning the
deployment object with cancellation_requested: true.
Cancellation is cooperative: Aetherfy flags the deployment and the builder stops at its next checkpoint. A deployment that already reached a terminal state cannot be cancelled.
| Code | HTTP | Meaning |
|---|---|---|
DEPLOYMENT_TERMINAL_CANNOT_CANCEL | 409 | Already active, failed, completed, superseded or rolled_back |
DEPLOYMENT_NOT_FOUND | 404 |
Reading the payload a run was started with
GET /api/v1/deployments/{deployment_id}/payload → 200 {"payload": { … }}
There are two ways to reach this route, and they differ only in where the
{deployment_id} comes from.
From inside the agent. A spawned or scheduled type: job agent calls it on
itself. Aetherfy injects AETHERFY_SPAWN_ID and AETHERFY_API_KEY into the
machine’s environment, and AETHERFY_SPAWN_ID is the deployment id to use:
curl -s "https://agents.aetherfy.com/api/v1/deployments/$AETHERFY_SPAWN_ID/payload" \
-H "Authorization: Bearer $AETHERFY_API_KEY"From outside, to inspect a run that already happened. The id of a row in
run history is the deployment id — the same value — so list the
runs and pass one straight through:
# The id of the most recent run.
RUN_ID=$(curl -s "https://agents.aetherfy.com/api/v1/agents/reporter/runs?limit=1" \
-H "Authorization: Bearer $AETHERFY_API_KEY" | jq -r '.[0].id')
curl -s "https://agents.aetherfy.com/api/v1/deployments/$RUN_ID/payload" \
-H "Authorization: Bearer $AETHERFY_API_KEY"A spawn’s id comes back as spawn_id on the
spawn response instead, since a spawned run does not appear in the
child’s run history.
payload is whatever the spawner sent, or {} if it sent nothing.
| Code | HTTP | Meaning |
|---|---|---|
DEPLOYMENT_NOT_EPHEMERAL | 404 | A normal deployment has no payload — only spawned and scheduled runs do |
DEPLOYMENT_NOT_FOUND | 404 |
Build logs are not available from the Aetherfy API
There is no endpoint for build output. Aetherfy does not currently expose one, and no
path under /deployments/ serves build logs.
To follow a build, poll GET /api/v1/deployments/{deployment_id} and read state,
queue_position and error_message; a failed build puts its reason in
error_message. For an agent’s runtime output, which is a different thing
entirely and does have a route, see GET /agents/{agent}/logs.
Linking an Aetherfy agent to a GitHub repository
Push-to-deploy is configured per agent, on top of an account-level GitHub connection
made once at /api/v1/auth/github.
POST /api/v1/agents/{agent}/github → 201
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/github \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repo":"acme/agents","branch":"main","root_dir":"reporter"}'| Field | Type | Default | Notes |
|---|---|---|---|
repo | string | — | Required. owner/repo |
branch | string | main | Pushes to this branch deploy |
root_dir | string | null | null | Subdirectory holding this agent’s code and aetherfy.yaml. Omit for a repository root |
The 201 response carries repo, branch, root_dir, webhook_id and
webhook_secret:
{
"repo": "acme/agents",
"branch": "main",
"root_dir": "reporter",
"webhook_id": "0000000",
"webhook_secret": "<shown once — store it now>"
}webhook_secret is returned in this response and nowhere else. Aetherfy has no
route that reads it back. Capture it when you link, and if you lose it, unlink and
link again to mint a new one.
GET /api/v1/agents/{agent}/github → 200 returns {linked, repo, branch, root_dir, webhook_id} — no secret.
DELETE /api/v1/agents/{agent}/github → 204 unlinks and removes the webhook.
| Code | HTTP | Meaning |
|---|---|---|
GITHUB_NOT_CONNECTED | 422 | Connect the account first — see Account and usage |
GITHUB_REPO_NOT_FOUND | 404 | The repository is not visible to the installation |
GITHUB_API_ERROR | 503 | GitHub was unreachable. Retry |
GITHUB_WEBHOOK_CREATE_FAILED | 422 | GitHub rejected the webhook — most often one already exists on the repository |
GITHUB_PERSISTENCE_FAILED | 500 | The link could not be stored |
See GitHub integration for the workflow.