Base URL: https://api.agidock.cloud. Full spec:
/openapi.json.
Within /v1, endpoints and response fields are **added, never renamed,
removed, or re-typed**. A breaking change would arrive under a /v2 prefix;
none is planned.
Authorization: Bearer agd_... — API key. This is your credential, and the
only one you will ever hold. The first is minted by your human in the
dashboard (onboarding); no call ever asks a key for an
authenticator code.Roles: read (GET only), write (every call for VMs, volumes,
email and tickets), both limited to the one project the key was minted
into; admin (write across every project in the
account, plus issuing and revoking keys, managing projects, and freezing or
deleting the account). An admin key belongs to the account, so each
project-scoped call names its project with the AGIdock-Project header —
see projects. An admin key may mint write and
read keys but never another admin (403 admin_key_human_only).
Revocation stops minting instantly; every other call on a revoked key can
keep working for up to a minute after it. Use write unless you need to
manage credentials or reach more than one project.
A new key's rate_limit may not exceed the platform maximum
(400 rate_limit_invalid), nor the limit of the key minting it
(403 rate_limit_not_downward).
``` GET /v1/account/keys list keys with last-used times; never secrets POST /v1/account/keys mint one (admin key, or your human): write or read into the project AGIdock-Project names; admin with no header DELETE /v1/account/keys/{id} revoke; minting on that key stops instantly (admin key, or your human) ```
The other credential in the spec, Bearer sess_..., is your human's dashboard
session; an API key cannot get one.
You will meet these in /openapi.json. They are for your human.
Human-only routes refuse every API key with 403 human_required, and no
retry changes that:
POST /v1/billing/topup, PUT /v1/billing/refill, and every
verb of /v1/billing/payment-method, including GET./v1/account/totp: enrolling one,
verifying a code, removing one.POST /v1/account/unfreeze, which also needs a
current authenticator code in the body.Two more groups are not human-only, but are for your human too:
POST /v1/account/login, GET /v1/account/login/confirm — the
magic-link flow that gives a human a sess_... token. The first sends mail
to your human's inbox; the second needs what that mail contains. A new
account is completed on the dashboard's signup page only;
POST /v1/account/signup/complete answers 403 here.POST /v1/account/export, DELETE /v1/account,
POST /v1/account/delete/cancel — GDPR export, account closure with a
14-day grace, and the cancel that reverses it. An API key can call them —
any key for the export, an admin key for the other two — but leave them to
your human.Every error body looks like:
{
"code": "credit_cutoff",
"detail": "credit balance is 0; top up via POST /v1/billing/topup — running VMs are unaffected",
"status": 402,
"title": "Payment Required"
}
Four fields, always all four. title and status repeat the HTTP status
line; read code and detail. Some errors add machine-readable fields beside
them at the top level, listed with the code below.
code is stable and machine-matchable; detail always states the fix. There
are more codes than the list below, and /openapi.json names
only a few per status: the list below is the ones to handle in code, and for
any other code, follow detail.
Codes you must handle:
credit_cutoff (402) — balance at/below zero. Mutating calls are blocked.
Reads stay open, and so do all of /v1/support and /v1/compliance and
the calls your human needs to top up, authenticator included. Running VMs
are not touched. After 7 days at zero they are stopped, disks intact, and
30 days after that anything still there is deleted. A top-up at any point
resumes everything.read_only_key (403) — the key has role read and the call mutates. Not
retryable: use a write key, or ask your human for one.admin_key_required (403) — the call affects more than one project: minting
or revoking keys, managing projects, freezing or deleting the account. Not
retryable with this key.project_required (400) — an admin key called a project-scoped route
without the AGIdock-Project header. Add it; no project is picked for you.key_project_mismatch (403) — a write or read key named a project other
than its own. Drop the header, or use a key minted into that project.quota_exceeded (403) — includes quota, used, requested, cap.rate_limited (429) — includes Retry-After. Default 120 req/min per key.idempotency_key_reuse (409) — same key, different request body.idempotency_in_progress (409) — an earlier request with this key is still
running. Nothing was executed twice. Retry the *same* key after the
Retry-After pause to collect its result; a fresh key would start a second
operation.hypervisor_error (409) — the hypervisor refused an operation; detail is
its own error text, unchanged. Some of these errors are permanent (a
configuration it will not accept), some are about timing (a guest still
booting when you asked it to reboot). If detail looks like a timeout,
retry once after a pause; otherwise fix the cause.operation_in_progress, volume_busy, storage_busy (409) — another call
is already working on that VM, that volume, or the bulk storage behind it.
Only one call at a time works on a resource; a second one is refused, not
queued, so expect this when you call in parallel. Nothing was applied.
Retry the identical request.service_saturated (503) — we are already running as many operations at
once as we can and declined to start another. Nothing was applied. Back off
further than for the 409s above: this one is about total load rather than a
resource you named.VM and volume calls have codes of their own — create_failed,
no_capacity_available, vm_not_stopped, volume_in_snapshots,
vm_has_snapshots, vm_has_volumes, volume_moved,
volume_already_grown, vm_retained, volume_retained, not_retained,
retention_expired and rescue_unavailable. VMs explains each next to the call that returns
it.
Retry policy: retry 5xx, 429, idempotency_in_progress, and the per-resource
timing codes (operation_in_progress, volume_busy, storage_busy) — honoring
Retry-After where present — with backoff. volume_moved needs a re-read
first and volume_already_grown a different size. Never retry any other 4xx
except after fixing what detail says.
Failures originating in a backing system — the hypervisor, the payment
provider, an unreachable internal service — answer 409, not 502, so their
detail reaches you unchanged. Read it: it says whether you have to fix
something or can retry.
Every mutating endpoint honors Idempotency-Key (any string up to 200
chars, scoped to your account, 24-hour window). Same key + same body →
the stored response is replayed, even across network failures. Same key +
different body → 409 idempotency_key_reuse. Always send one: it is what
makes a retry after a timeout safe.
The key is recorded before the work starts, so this holds while the first
request is still running too: a retry sent before it finishes gets 409
idempotency_in_progress instead of starting the operation again. That is the
usual answer when a create takes longer than your client's timeout — keep
retrying the same key and one of the attempts returns the stored result.
Only one route is paginated: GET /v1/billing/ledger, newest entry first.
GET /v1/billing/ledger?limit=100
-> {"entries": [...], "next_cursor": 8412}
GET /v1/billing/ledger?limit=100&cursor=8412 the next page, older
next_cursor is the id of the last entry on the page, not a flag for "more
exist" — it is zero only when the page came back empty. **Stop when entries
is shorter than limit**; waiting for a zero costs one more request for a page
with nothing in it. limit defaults to 100, and anything outside 1-500 is
read as absent: limit=1000 returns 100, not 500. Filters (?kind=,
?exclude=) are per request, so repeat them on every page.
Nothing else is paginated. Three lists return their newest 100 with no way to
get older entries: GET /v1/compliance/cases, GET /v1/support/tickets and
GET /v1/vms/{id}/events. GET /v1/vms, /v1/volumes, /v1/images and
/v1/account/keys return everything you have, uncapped. GET /v1/usage is
not a list at all — it aggregates over the from/to window you pass.
IDs are opaque strings carrying a type prefix (vm_..., vol_...,
key_...) and sort by creation time. Read the prefix if you need the type,
and do not parse anything after it.
The fields each resource returns are in its guide: VMs, email, the account.