Skip to Content
Agent computeREST: deployments
Raw

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 "[email protected]"
ParameterInNotes
code_archivemultipart formRequired. Gzipped tarball of the agent directory
confirm_overagequeryDefault false. Set true to accept a deploy that adds usage

aetherfy.yaml must sit at the archive root: it is the whole description of how the release runs (runtime, type, memory, always-on, idle timeout, regions, schedule, spawn), and a field it leaves out takes its default, never the agent’s previous value. The release records that configuration, and a later rollback or redeploy of it runs exactly that again. An archive without it is refused with 422 DEPLOYMENT_CONFIG_PARSE_ERROR and the sentence “Missing required aetherfy.yaml. Add it beside your code with at least runtime:; every field it leaves out takes its default.” A file naming name, description or workspace is refused the same way. See the aetherfy.yaml reference.

The agent’s workspace, name, description, grants and secrets are the platform’s, not the file’s: a deploy goes to the agent’s own workspace and never moves it.

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 "[email protected]"

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.

CodeHTTPMeaning
SOFT_CAP_EXCEEDED403A spend limit was reached. Existing agents keep running; a deploy goes through only if it reduces cost and grows nothing — see Deploying while growth is frozen. detail.field names the first setting that would grow, and the message ends “While your account is frozen a deploy must reduce cost and grow nothing: turn always-on off, lower memory, use fewer regions or shorten the idle timeout.”
DUNNING_FROZEN403A payment failed and the account is in its grace period. The same reduce-only rule applies
OVERAGE_CONFIRM_REQUIRED402Confirm the added cost — see above
PLAN_LIMIT_EXCEEDED403Agent count, memory, regions, always-on or the runtime exceeds the plan. detail.limit names which; for runtime: dockerfile on a plan without custom Dockerfile builds it is dockerfile_runtime_allowed: “Your plan does not support custom Dockerfile builds. Switch to a managed runtime or upgrade.” Nothing is built
AGENT_SAFETY_LIMIT_EXCEEDED403The account already holds the most deployed agents any account may have (100). It is not a plan limit, so there is nothing to upgrade to: delete or archive a deployed agent, or contact support. detail.limit is agent_safety_limit and detail.support_url points to support
DEPLOY_REGIONS_NOT_IN_SCOPE403Requested regions are outside the workspace’s region set
INVALID_REGION400A regions: entry is not a region Aetherfy runs in. The body’s regions array names the bad ones
WORKSPACE_HAS_NO_REGIONS400The agent’s workspace has no regions yet, so there is nowhere to deploy. Set the workspace’s regions first
AGENT_PAUSED_CANNOT_DEPLOY409Start the agent first
DEPLOYMENT_IN_PROGRESS409A build is already running. Transient — retry
DEPLOYMENT_ARCHIVE_TOO_LARGE413The upload exceeded the archive cap
DEPLOYMENT_CONFIG_PARSE_ERROR422aetherfy.yaml is missing, malformed or invalid, or names name, description or workspace. The message says which, and where a platform field is set instead
RUNTIME_IMMUTABLE422An agent’s runtime cannot change after its first deploy
WORKER_NOT_FOUND422A name in spawn.workers is not a live agent on your account. ["*"] names none and is not checked
AGENT_SPAWN_WORKERS_REQUIRED422spawn.enabled is true and the file lists no spawn.workers. List them, or ["*"] for any
DEPLOYMENT_UPLOAD_FAILED500Aetherfy could not store the archive. Retry

The Dockerfile rule holds on every path that makes a deployment, not only this upload: a redeploy, a rollback to a Dockerfile release, a move, and a run, spawn or scheduled task are refused with the same 403, and a push to a linked repository ends as a failed deployment whose error_message is the same sentence. See the Dockerfile runtime.

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.

What an Aetherfy agent can be deployed with

GET /api/v1/deploy-options → 200 lists the regions, runtimes and memory sizes an agent can declare in aetherfy.yaml, each with whether it is allowed. With ?workspace_id=, each region’s allowed is the deploy’s own verdict for an agent in that workspace: your plan and the workspace’s regions. Without it, allowed is your plan’s verdict alone. afy init and the dashboard’s configurator offer what it allows.

curl -s "https://agents.aetherfy.com/api/v1/deploy-options?workspace_id=$WORKSPACE_ID" \ -H "Authorization: Bearer $AETHERFY_API_KEY"
{ "regions": [ {"code": "us-east-1", "allowed": true}, {"code": "eu-central-1", "allowed": true}, {"code": "ap-southeast-1", "allowed": false} ], "workspace_id": "d4b1c8f7-2e05-4a96-8b13-7f0c6e29a5d3", "runtimes": ["python3.11", "python3.12", "python3.13", "node20", "node22", "node20-ts", "node22-ts", "bun", "dockerfile"], "max_regions": 3, "memory_mb": [ {"memory_mb": 256, "allowed": true}, {"memory_mb": 512, "allowed": true}, {"memory_mb": 1024, "allowed": true}, {"memory_mb": 2048, "allowed": true}, {"memory_mb": 4096, "allowed": true}, {"memory_mb": 8192, "allowed": false} ], "max_memory_mb": 4096 }

A workspace_id that is not one of your workspaces is 404 WORKSPACE_NOT_FOUND, the same answer for another account’s id as for one that does not exist.

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, "release_version": null, "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, "result": null, "result_error": null, "has_result": false, "created_at": "2026-08-19T09:04:11Z", "started_at": "2026-08-19T09:04:19Z", "deployed_at": "2026-08-19T09:06:02Z", "error_message": null, "build_failure_detail": null }

Aetherfy deployment states

StateMeaning
queuedWaiting for a builder. queue_position is set
buildingBuilding the image
deployingBuilt; machines coming up, then health-checked until each answers (up to 5 minutes). The version it replaces keeps serving meanwhile
activeServing
failedTerminal. Read error_message, and build_failure_detail when the build stage is what failed
rolled_backSuperseded by a rollback to an earlier version
supersededA newer deployment replaced it
completedTerminal, and not a failure — an ephemeral run that finished

A deployment reads the agent’s secrets once, before any machine exists, and every machine it creates gets the same set. If they cannot be read, the read is retried in place, four attempts 1, 2 and 4 seconds apart; the deployment is never queued again. When every attempt fails it ends failed with nothing built, and error_message reads “Couldn’t read this agent’s secrets, so it wasn’t deployed. Retry the deploy.” It carries no code: deploy again. A region still pending stays pending and retries on its own schedule. See Secrets.

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.

release_version is set on a run: the version of the release it executed, which is not the run’s own version — that is only its place in the agent’s history. It is null on a release, on a service run no machine accepted, and on runs recorded before Aetherfy kept it.

is_serving is true exactly when the deployment is active: every machine it started has answered its health check. While a new deployment is deploying, the version it replaces is the one serving. An active deployment can still be missing a region that failed to launch, 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.

result carries what an ephemeral run returned, and result_error says why it did not — see the result path. Both are empty on a normal deployment and on a run that returned nothing. The two list routes above omit result and send has_result instead: a list can hold hundreds of runs, so read the value from GET /api/v1/deployments/{deployment_id} for the one run you want it from.

CodeHTTPMeaning
DEPLOYMENT_NOT_FOUND404Unknown id, or it belongs to another account
DEPLOYMENT_ACCESS_DENIED403The 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 the version’s built image when it still exists, and otherwise rebuilds the version from its stored code archive. It creates a new version rather than reviving the old number. A version with neither is not a valid target, and neither is any version of an archived agent: restore the agent first.

CodeHTTPMeaning
DEPLOYMENT_ROLLBACK_TARGET_INVALID422That version is not a rollback target — it has neither its image nor its code archive. can_rollback on the deployment object answers this before you call
DEPLOYMENT_NOT_FOUND404No such version for this agent
DEPLOYMENT_IN_PROGRESS409A build is running. Transient — retry
AGENT_PAUSED_CANNOT_ROLLBACK409Start the agent first
AGENT_ALREADY_ARCHIVED409The agent is archived. Run afy restore <agent> first
SOFT_CAP_EXCEEDED / DUNNING_FROZEN403The account is frozen and this release would not reduce cost — see Deploying while growth 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, with no build. Neither carries old secrets. Every new deployment, a redeploy or a rollback included, reads the agent’s secrets as they are now, once, before it creates any machine; an image never holds secrets. So redeploying, or rolling back, is how a stored secret reaches a running service. A redeploy creates a new version.

A release is its code and the configuration it was deployed with, so a redeploy — like a rollback — runs that version’s own memory, always-on, regions and the rest, not the agent’s latest. A version from before Aetherfy kept each release’s configuration is refused with RELEASE_CONFIG_NOT_ON_RECORD: deploy it again from its source.

Archives are not kept forever: Aetherfy stores them for the ten most recent successful deployments (and always for the one the agent is running), and a version whose build fails keeps none. An archive another live or in-flight deployment still uses is kept while it does, whichever version it was uploaded for. A version an exact rollback created has no archive of its own: it is rebuilt from the archive of the version it rolled back to. can_redeploy on the deployment object tells you whether a given version still has its source; read it rather than inferring from the state. It is false for every version of an archived agent, which must be restored before it can be redeployed.

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 building it again, with the agent’s current secrets, when you have no newer code to deploy. The one failure it does cover is a deployment that built but failed to launch, which keeps its archive.

CodeHTTPMeaning
DEPLOYMENT_REDEPLOY_SOURCE_UNAVAILABLE422That version’s stored code archive is gone, so it cannot be rebuilt
DEPLOYMENT_NOT_FOUND404No such version for this agent
DEPLOYMENT_IN_PROGRESS409A build is running. Transient — retry
AGENT_PAUSED_CANNOT_REDEPLOY409Start the agent first
AGENT_ALREADY_ARCHIVED409The agent is archived. Run afy restore <agent> first
SOFT_CAP_EXCEEDED / DUNNING_FROZEN403The account is frozen and this release would not reduce cost — see Deploying while growth 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 it stops at its next checkpoint, including during the health check that ends deploying. The machines it started are removed and the version it was replacing keeps serving. A deployment that already reached a terminal state cannot be cancelled.

Aetherfy also stops a deployment itself when the agent is paused, or the account is frozen (a spend limit reached, or a payment overdue), before the new version goes live: it ends failed with cancellation_reason agent_held or account_frozen and an error_message saying which, its machines are removed, and the version it was replacing is unchanged. If the agent is paused, or the account’s agents are paused for usage, just after the new version passed its health check, that version goes active but stopped, and it is the one that runs when the agent resumes. A freeze alone (a payment overdue, or a spend limit that only blocks new deploys) does not stop it: it goes active and runs, like the account’s other agents.

A stopped deployment carries one cancellation_reason, the first one recorded, and an error_message that says the same thing in a sentence. Switch on the value, not on the message:

cancellation_reasonSet when
user_cancelYou cancelled it with the route above
agent_deletedIts agent was deleted, or was being deleted, before it finished. Nothing is left running
agent_archivedIts agent was archived, or was being archived, before it could start a machine. Nothing is left running
parent_deletedA spawned run whose parent agent was deleted. Nothing is left running
parent_archivedA spawned run whose parent agent was archived. Nothing is left running
agent_heldIts agent was paused, or stopped at its spend limit, before the new version served. The version it was replacing is unchanged
account_frozenThe account reached its spend limit, or has a payment overdue, before the new version served. The version it was replacing is unchanged
CodeHTTPMeaning
DEPLOYMENT_TERMINAL_CANNOT_CANCEL409Already active, failed, completed, superseded or rolled_back
DEPLOYMENT_NOT_FOUND404

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.

CodeHTTPMeaning
DEPLOYMENT_NOT_EPHEMERAL404A normal deployment has no payload — only spawned and scheduled runs do
DEPLOYMENT_NOT_FOUND404

Waiting for an Aetherfy run to finish

GET /api/v1/deployments/{deployment_id}/wait → 200, the same deployment object as the read above.

Aetherfy holds the request until the run has finished, then answers immediately. This is how a caller collects a run’s result without polling in a loop.

?timeout_seconds= is how long Aetherfy may hold it: 1 to 60, default 30.

curl -s "https://agents.aetherfy.com/api/v1/deployments/$RUN_ID/wait?timeout_seconds=45" \ -H "Authorization: Bearer $AETHERFY_API_KEY"

A timeout is not an error. If the run has not finished in time, Aetherfy answers 200 with the run exactly as it stands, and its state is still active. Read state: a terminal one means the run is over and result is final, active means call again. Waiting longer than 60 seconds is a second call, not a bigger number — the request is held open, and anything longer is cut by the network in front of Aetherfy.

The same route works on a normal deployment, where “finished” means the version reached active.

CodeHTTPMeaning
DEPLOYMENT_WAIT_TIMEOUT_INVALID422timeout_seconds was outside 1 to 60
DEPLOYMENT_NOT_FOUND404Unknown id
DEPLOYMENT_ACCESS_DENIED403The deployment’s agent is not yours

The last two are exactly what the deployment read above answers, and deliberately so: the two routes return the same object, so they refuse the same way.

The complete build log is not available from the Aetherfy API

There is no endpoint for a complete build log. 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 short reason in error_message, and the step it failed on in build_failure_detail: the tail of the build output, redacted and capped. That field is null when the deployment failed anywhere other than the build stage, so its absence is an answer rather than a gap.

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"}'
FieldTypeDefaultNotes
repostring—Required. owner/repo. The owner is the account one of your installations is on, which is an organization as often as a person and need not be your own login. GET /api/v1/auth/github/repositories returns exactly the values this accepts, across every connected account; Aetherfy records which installation answered for the repository and deploys this agent through it from then on.
branchstringmainPushes to this branch deploy
root_dirstring | nullnullSubdirectory 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, account_connected, installation_id, account_login, branch_deleted_at, waiting_push} — no secret. Every agent object carries the same object as github, so a client that already reads the agent needs no second request for its link.

branch_deleted_at is when Aetherfy last saw the tracked branch deleted, or null. It is set from the push webhook GitHub sends for a deletion and cleared by the next push to that branch. It is the only place that fact is reported: a deletion carries no commit, so there is nothing for Aetherfy to attach a check to. Null means no deletion has been seen, which covers both an agent that never lost its branch and one that got it back.

waiting_push is the push waiting for the deploy in progress, as {sha, received_at}, or null when none waits. An agent deploys one push at a time: a push that arrives while one of its deployments is queued, building or deploying waits, and has no deployment until it starts, so this is the only place the API shows it. Where several wait, it is the one that will deploy, the newest commit; it starts when the deploy in progress ends (pushing while a deploy is running).

account_connected answers what linked cannot: whether a push would actually deploy. A link survives a disconnect on purpose, so that reconnecting resumes deployment without relinking every agent, which leaves a real state where linked is true and pushes are skipped. When it is false, reconnect the account; relinking the agent is the wrong remedy and fails for the same reason.

It is per AGENT, not per Aetherfy account. One Aetherfy account can hold several GitHub installations, and an agent deploys through the one whose repositories include its own — so disconnecting an organization leaves the agents on your personal repositories deploying. installation_id and account_login name that installation and its GitHub account, which is what lets a client say WHICH account to reconnect. Both are null when the agent is not linked. DELETE /api/v1/agents/{agent}/github → 204 unlinks and removes the webhook.

The Aetherfy webhook receiver answers 204 once the delivery is recorded, which is before the deploy starts: the code means Aetherfy has the push, not that it has deployed it. What that buys, and why redelivering is always safe, is on GitHub integration.

CodeHTTPMeaning
GITHUB_NOT_CONNECTED422Connect the account first — see Account and usage
GITHUB_REPO_NOT_FOUND404The repository is not visible to the installation
GITHUB_API_ERROR503GitHub was unreachable. Retry
GITHUB_WEBHOOK_CREATE_FAILED422GitHub rejected the webhook — most often one already exists on the repository
GITHUB_PERSISTENCE_FAILED500The link could not be stored
GITHUB_DELIVERY_NOT_RECORDED500A verified push delivery could not be recorded, so Aetherfy refused it rather than accepting a push it might not finish. Redeliver it from the repository’s webhook settings

See GitHub integration for the workflow.

Last updated on