A refusal is a response like any other: it has a stable shape, a code your program can read, and an identifier support can trace. Nothing useful hides in the message text.
The shape of a refusal
{
"error": {
"code": "insufficient_credit",
"message": "This order needs 0.44 CAD of credit; the balance is 0.12 CAD.",
"details": { "required": "0.44", "balance": "0.12", "missing": "0.32", "currency": "CAD" },
"request_id": "req_8fh2kq4m1x0a"
}
}
Branch on code, never on message: the message is
English, meant for a log, and may be rephrased. The code is part of the
contract. details carries what you need to act — here, the exact
amount to top up.
Every response, successful or not, carries an X-Request-Id
header. Quote it when you write to us and we find the call.
The codes
New codes may appear. Treat an unknown one as an ordinary failure of its HTTP class.
| Code | HTTP |
|---|---|
missing_token | 401 |
invalid_api_key | 401 |
key_disabled | 401 |
key_expired | 401 |
insufficient_scope | 403 |
ip_not_allowed | 403 |
email_not_verified | 403 |
account_suspended | 403 |
forbidden | 403 |
validation_failed | 400 |
not_found | 404 |
request_refused | 422 |
insufficient_credit | 402 |
budget_cap_reached | 402 |
payment_required | 402 |
quota_reached | 409 |
service_suspended | 409 |
action_conflict | 409 |
idempotency_conflict | 409 |
hostname_taken | 409 |
order_in_progress | 409 |
too_many_pending_invoices | 409 |
coupon_not_found | 422 |
coupon_max_uses | 422 |
plan_unavailable_in_region | 422 |
image_unavailable_in_region | 422 |
image_incompatible_with_plan | 422 |
out_of_stock | 422 |
billing_mode_unavailable | 422 |
provisioning_failed | 500 |
rate_limited | 429 |
backend_unavailable | 503 |
internal_error | 500 |
Rate buckets
Per key: 120 reads a minute, 20 actions a minute, and 10 creations or reinstalls an hour. A reinstall destroys a disk and rebuilds a machine, so it falls in the same bucket as a creation.
Every response carries X-RateLimit-Limit and
X-RateLimit-Remaining for the bucket the call fell in; a refusal
answers 429 with Retry-After. A wider guard also
applies per calling address, before authentication: a normal client never
meets it, a script guessing tokens does.
Replaying without duplicating
POST /v1/vms requires an Idempotency-Key header: a
value of your choosing, unique per order. If your request times out you do
not know whether the machine was created — replaying with the same key
returns the first response, as it was, with
Idempotency-Replayed: true.
Three cases, one of them a replay: same key and same body, we serve it again;
same key, different body, that is a mistake on your side and we refuse
(409 idempotency_conflict); same key while the first is still
running, we refuse rather than run both. Keys are kept for 24 hours.
Asynchronous actions
Provisioning, reinstalling, powering on: none of it is instant. Those calls
answer 202 and an action object, which you poll
until its status leaves running.
{ "id": 90112, "type": "create", "status": "error", "vm_id": 4312,
"source": "api",
"error": { "code": "provisioning_failed",
"message": "No IPv4 address left in the pool for this region." } }
An action does not stay open forever: after six hours without a conclusion —
a lost task, a machine deleted before anyone read its fate — it turns to
error with the code internal_error. A polling loop
therefore always ends.
A machine's history is on GET /v1/vms/{id}/actions, and it is
complete: a click in the console shows up there too, with
source: console.