# Errors and exit codes

Machine-readable: https://innstance.impossibuild.ai/docs/errors.json

## The error envelope

Every JSON error body has the same envelope:

    {"error": "<message>", "code": "<CODE>", "retryable": false, "docs_url": "<this page>#<CODE>"}

- error: the human sentence. Print it as-is; clients that read only this field keep working.
- code: stable once sent; branch on it, never on the sentence. A refusal with no code of its own carries the generic code for its HTTP status.
- retryable: true when the same request, sent again after a pause, can succeed with nothing changed. Honour Retry-After when it is set.
- docs_url: this catalog, at the code.

Some codes add fields of their own (upgrade, retry_after, over_pool_until, moderation, ...). Codes are case-sensitive; a few early subscription codes are lower case and stay that way.

Not every response is JSON: a body over a route's size cap is refused with a plain-text 413 before any handler runs, and an unknown route is a plain-text 404.

## Error codes

| Code | HTTP | Retryable | Meaning | What to do |
|---|---|---|---|---|
| `INVALID_REQUEST` | 400 | no | (generic) The request is malformed or a value is not allowed. | Read the message: it names the field and the rule. Fix the request and send it again. |
| `UNAUTHENTICATED` | 401 | no | (generic) No credential, or the token is unknown, revoked or expired. | Sign in again (innstance login) or send a valid token as Authorization: Bearer <token>. |
| `PAYMENT_REQUIRED` | 402 | no | (generic) The request needs payment or more balance first. | Pay or top up as the body describes (x402 challenges carry an accepts list), then send the request again. |
| `FORBIDDEN` | 403 | no | (generic) This account may not do this. | Do not retry. The message says why; a cookie request from another origin is refused here too. |
| `NOT_FOUND` | 404 | no | (generic) No such resource on this account (another account's resources also answer 404). | Check the name; list what exists (GET /apps, /sites, /agents/hosts). |
| `REQUEST_TIMEOUT` | 408 | yes | (generic) The request was cancelled before the operation finished. | Read the resource's state, then send the request again if it did not take effect. |
| `CONFLICT` | 409 | no | (generic) The request conflicts with the resource's current state: a name already taken, a limit on tokens, a resource in the wrong state. | Change the request or the resource as the message says; waiting alone does not clear it. A conflict that does clear by itself, after which the same request passes, has its own retryable code (OPERATION_IN_PROGRESS, POOL_LOCKED, DEPLOY_IN_PROGRESS, APP_OPERATION_IN_PROGRESS). When the work asked for is already under way (an assistant being set up), watch the resource instead of sending the request again. |
| `GONE` | 410 | no | (generic) What the request refers to existed but has expired or been used. | Start the flow again to get a fresh link, token or session. |
| `PAYLOAD_TOO_LARGE` | 413 | no | (generic) The request body is larger than this route accepts. | Send less. JSON routes take up to 64 KiB; upload routes state their plan's limit in the message. |
| `UNPROCESSABLE` | 422 | no | (generic) The request is well formed but its content cannot be accepted. | Change the content as the message says. |
| `RATE_LIMIT` | 429 | yes | (generic) Too many requests from this account or address. | Wait for Retry-After seconds (also in retry_after or retry_after_seconds when present), then send the request again. |
| `INTERNAL_ERROR` | 500 | yes | (generic) The server failed while handling the request. | Retry with backoff. For a write, read the resource first: part of it may have happened. |
| `UPSTREAM_ERROR` | 502 | yes | (generic) A provider the platform depends on (machines, certificates, payments, DNS) failed or answered something unusable. | Retry with backoff. Failures whose outcome is unknown use OUTCOME_UNKNOWN instead. |
| `UNAVAILABLE` | 503 | yes | (generic) A dependency is briefly unavailable, or coordination with another request failed. | Retry after a short pause. A feature this deployment does not run answers NOT_ENABLED instead. |
| `TIMEOUT` | 504 | yes | (generic) The operation ran out of time. | Read the resource's state, then retry if it did not finish. Work run on a machine, which may still be running there, answers EXEC_TIMEOUT instead. |
| `OPERATION_IN_PROGRESS` | 409 | yes | Another change to the same app, site or assistant is running, or it is still starting. | Wait a few seconds and send the same request again. |
| `NOT_ENABLED` | 503 | no | This deployment does not run the feature (it is switched off in its configuration). Sent by: assistants and their settings changes, backups, volumes, custom domains, legacy CLI login, card checkout, traffic top-ups, deploys without a runner image. | Do not retry; use another way, or ask the operator. A few older messages call this "temporarily unavailable"; the code is what holds. |
| `OUTCOME_UNKNOWN` | 500, 502, 503 | no | The request may already have taken effect, in whole or in part, and the server cannot confirm it. Sent by: app create, commands run with exec, runner upload completion, subscription plan changes, x402 top-ups whose settlement did not answer or answered something unusable. | Do not repeat it blindly: read the target's state first (the message says what to check), then decide. |
| `ACCOUNT_SUSPENDED` | 403 | no | The account is suspended for breaking the acceptable use policy. Sent by: every sign-in route. | Do not retry. The body carries the reason and an appeal link when there is one. |
| `ACCOUNT_CHANGED` | 403 | no | The browser session now belongs to a different account than the page expected. | Reload the page and confirm the action with the intended account. |
| `TOKEN_REQUEST_REPLAY` | 409 | no | This token request already created a token; its secret cannot be shown again. Sent by: POST /auth/token. | Check GET /auth/tokens and revoke the token if its secret was not received. |
| `RATE_LIMIT_UNAVAILABLE` | 503 | yes | The rate limiter could not record the request, so an expensive route refused it. | Retry after a short pause. |
| `FREE_TIER_DISABLED` | 403 | no | New free accounts are switched off on this server. | Subscribe to a paid plan (upgrade names the cheapest). |
| `FREE_TIER_FULL` | 403 | no | The free plan has reached its account cap. | Subscribe to a paid plan. |
| `APP_LIMIT` | 403 | no | The plan's app count is reached. Sent by: app create. | Remove an app, or upgrade to the plan named in upgrade. |
| `SITE_LIMIT` | 403 | no | The plan's published-site count is reached. Sent by: site create. | Remove a site (innstance sites rm), or upgrade to the plan named in upgrade. |
| `CPU_LIMIT` | 403 | no | More CPUs per machine than the plan allows. Sent by: app create and update. | Ask for fewer CPUs, or upgrade. |
| `CPU_KIND_LIMIT` | 403 | no | Performance CPUs are not included in this plan. Sent by: app create and update. | Use cpu_kind shared, or upgrade. |
| `POOL_MEMORY_LIMIT` | 403 | no | The change would take the account over its plan's RAM pool. Sent by: app create and update, scale, assistant launch. | Free RAM (smaller machines, fewer apps or assistants), or upgrade. |
| `POOL_VOLUME_LIMIT` | 403 | no | The change would take the account over its plan's storage pool. Sent by: volume create and extend, assistant launch. | Remove or shrink volumes, or upgrade. |
| `VOLUMES_NOT_AVAILABLE` | 403 | no | The plan does not include persistent volumes. Sent by: volume create. | Upgrade, or keep state in a managed database. |
| `AUTOSCALE_LIMIT` | 403 | no | More machines per app than the plan allows. Sent by: app create and update, scale. | Ask for fewer, or upgrade. |
| `DOMAINS_NOT_AVAILABLE` | 403 | no | The plan does not include custom domains. Sent by: domain add. | Upgrade. |
| `DOMAIN_LIMIT` | 403 | no | The plan's custom-domain count is reached. Sent by: domain add. | Remove a domain, or upgrade. |
| `ALWAYS_ON_PERFORMANCE` | 403 | no | The change would add always-on performance CPU, which this account's plan does not include. Sent by: deploy, apply, scale. | Follow one of the fixes in the body. |
| `ALWAYS_ON_COST_LIMIT` | 403 | no | The change would take the account's always-on machines past the plan's always-on budget. Sent by: deploy, apply, scale. | Follow one of the fixes in the body. |
| `LIMITS_UNAVAILABLE` | 403, 503 | yes | Usage could not be measured, so a change that needs the pool check was refused. | Retry in a moment. |
| `OVER_POOL` | 402 | no | The account is over its plan's pool; over_pool_until is when the grace period ends. | Reduce usage or upgrade before making further changes. |
| `POOL_LOCKED` | 409 | yes | Another change to this account's pool is being written. | Retry with a short jittered backoff; the CLI does this itself. |
| `TRIAL_EXPIRED` | 403 | no | The free trial for this app or site has ended. Sent by: deploy, site upload. | Upgrade (url in the body) to deploy or publish again. |
| `SETUP_LIMIT` | 409 | no | Too many names are held by pages or assistants that were never finished. Sent by: site create, assistant launch. | Finish or remove one of them, then try again. |
| `NAME_TAKEN` | 409 | no | The name is taken on the public namespace. | Pick another name. |
| `NAME_NOT_ALLOWED` | 400 | no | The name resembles a well-known brand or the platform, or promotes gambling, adult content or scams. | Pick a plainly different name; the reply never says which rule matched. |
| `agent_name_yours` | 409 | no | This account already has an assistant with this name; status says what state it is in. Sent by: POST /agents. | Carry on with it (send its inputs), or destroy it first. |
| `agent_name_taken` | 409 | no | The name is held by another account, or by one of this account's sites or apps. Sent by: POST /agents. | Pick another name. |
| `UNDER_REVIEW` | 403 | no | The resource is held or quarantined by trust and safety. | Do not retry; it cannot be changed or deleted until reviewed. moderation carries the reasons and the appeal link. |
| `FILE_TYPE_NOT_ALLOWED` | 422 | no | The free plan publishes only web file types; files lists the refused ones. Sent by: site upload. | Remove those files, or upgrade. Nothing was published. |
| `IMAGE_NOT_CHECKABLE` | 422 | no | An image could not be checked (AVIF, HEIC, JPEG XL, or far larger than a page needs). Sent by: site upload. | Save it as PNG, JPEG, GIF or WebP. Nothing was published. |
| `DEPLOY_IN_PROGRESS` | 409 | yes | A deploy is already running on this app. Sent by: runner deploy, app delete. | Wait for it to finish (GET /apps/{name}/deployments), then retry. |
| `APP_OPERATION_IN_PROGRESS` | 409 | yes | Another deploy or app-wide operation is running. | Retry when it finishes. |
| `DELETE_FAILED` | 502 | yes | Tearing the app down at the provider failed; the app is kept as delete_failed. | Call DELETE again (retry carries a hint). |
| `EXEC_TIMEOUT` | 504 | no | The command ran out of time. It may still be running on the machine, or have finished there. Sent by: POST /apps/{name}/exec, POST /agents/hosts/{name}/exec, POST /apps/{name}/pull (archiving). | Do not send the same request again: it would run the work a second time and run out of time the same way. Start a long command detached (`nohup <cmd> > /tmp/log 2>&1 < /dev/null &`) and poll it with short commands; `innstance agents ssh` has no time limit. For a backup, choose a smaller directory. |
| `EXEC_OUTPUT_TOO_LARGE` | 502 | no | The command ran, but its output was larger than an exec response can carry. Sent by: POST /apps/{name}/exec. | Do not run it again as is: redirect its output to a file and fetch the file (`innstance machines pull`). |
| `DOMAIN_PROOF_REQUIRED` | 428 | yes | Ownership of the hostname is not proven yet; nothing was written. Sent by: domain add (apps and sites). | Publish the TXT record in verify (and the CNAME in records), then send the same request again. |
| `PAYMENT_NOT_CREDITED` | 500 | no | The payment settled on-chain, but the account could not be credited with it. Its transaction (tx_hash) is kept for reconciliation. Sent by: x402 top-ups (GET /subscription/topup/x402, GET /billing/egress/topup/x402). | Do not pay again: the same request cannot succeed, and a fresh payment would be a second one. Check GET /subscription/topup/recent (plan days) or GET /billing/usage (traffic); if the credit has not appeared, contact support with tx_hash. |
| `PRICING_NOT_ON_SALE` | 409 | no | There is no other pricing version on sale here, so the account's plan is the one on sale. Sent by: POST /billing/pricing. | Nothing to do. |
| `PRICING_SWITCH_NOT_ALLOWED` | 409 | no | A switch is one way: a pricing version that is no longer sold cannot be chosen again. Sent by: POST /billing/pricing. | Nothing to do; the account keeps its version. |
| `PRICING_SWITCH_DOES_NOT_FIT` | 409 | no | The account does not fit the new version's plan yet; blockers lists what stands in the way. Nothing was changed. Sent by: POST /billing/pricing. | Act on each blocker, check again with GET /billing/pricing, then send the switch again. |
| `PRICING_VERSION_CHANGED` | 409 | no | The account's plan changed while it was switching; nothing was written. Sent by: POST /billing/pricing. | Read GET /billing/pricing again and switch only if its preview still holds. |
| `checkout_in_progress` | 409 | yes | A checkout is already open for this account. | Finish or abandon it, then retry. |
| `crypto_subscription_active` | 409 | no | An active crypto subscription must be cancelled before a checkout. Sent by: POST /subscription/checkout. | Cancel it first. |
| `already_on_plan` | 409 | no | The account is already on the requested plan. | Nothing to do. |
| `cancel_first` | 409 | no | The account has a different recurring plan. Sent by: POST /subscription/checkout. | Switch with POST /subscription/change-plan, or cancel first (the paid period is kept). |
| `card_coming_soon` | 503 | no | Card checkout is not available on this deployment. | Pay with USDC instead (innstance billing topup). |
| `scheduled_change_active` | 409 | no | A plan change is already scheduled; scheduled_tier and scheduled_change_at say which and when. | Call it off (POST /subscription/change-plan/cancel) before scheduling another. |
| `not_configured` | 503 | no | Plan changes need the payment gateway, which this deployment lacks. Sent by: subscription plan changes. | Do not retry. |
| `no_subscription` | 409 | no | There is no active recurring subscription to change. | Subscribe with POST /subscription/checkout instead. |
| `billing_state_mismatch` | 409 | no | The platform's record of the plan and the payment gateway's disagree. | Do not retry; contact support to reconcile billing. |
| `change_reconciliation_pending` | 409 | yes | A previous plan change is still being reconciled with the gateway. | Retry once it has landed (a few minutes). |
| `payment_declined` | 402 | no | The upgrade charge was declined. | Check the payment method, or cancel and subscribe to the new plan. |
| `no_scheduled_change` | 409 | no | No plan change is scheduled. | Nothing to call off. |
| `change_not_cancelable` | 409 | no | The scheduled change is already being applied. | Pick a plan again once it has landed. |
| `payments_paused` | 503 | yes | USDC payments are paused on this server. Nothing was charged. | Retry after the time in Retry-After, or pay by card where the plan offers it. |
| `PERFORMANCE_ADDON_LIMIT` | 403 | no | This change needs more performance machines than the account's add-ons cover, one add-on per machine. | Use shared CPU, or buy the add-on the message names (innstance billing addons). |
| `unknown_addon` | 400 | no | There is no add-on with that ID. | Use an ID from the message, or from GET /billing/plans. |
| `addon_not_found` | 404 | no | The account holds no active add-on in that slot. | List the account's add-ons (innstance billing addons). |
| `addon_not_for_plan` | 403 | no | This plan already includes performance CPU, so the add-on would add only its RAM. | Do not buy it on this plan; the message says where it is sold. |
| `addon_needs_paid_plan` | 403 | no | The add-on is for paid plans. | Subscribe to the plan the message names, then buy the add-on. |
| `addon_not_on_sale` | 503 | no | This add-on cannot be bought on this server yet. | Do not retry. GET /billing/plans says what is on sale. |
| `addon_does_not_renew` | 409 | no | This add-on was a one-time purchase, so there is nothing to cancel. | Nothing to do: it ends on the date in ends_at. |
| `addon_slot_ambiguous` | 409 | no | Several renewing add-ons match. | Send the request again naming one slot from slots. |
| `UPLOAD_IN_PROGRESS` | 409 | yes | Another request is uploading this file right now. | Wait for it, then finalize. |
| `UPLOAD_ALREADY_RECEIVED` | 409 | no | This file was already uploaded to the session; each upload URL takes one upload. | Finalize, or start a new session to send different bytes. |
| `UPLOAD_INCOMPLETE` | 409 | no | Some of the session's files have not been uploaded. | PUT the files listed in missing to their upload_url, then finalize again. |

## CLI exit codes

| Exit status | Meaning |
|---|---|
| 0 | Success. `domains check` exits 0 only when the domain is configured and the site answers. |
| 1 | The command failed; the reason is printed to stderr as `Error: <message>`. This covers API errors (the message carries the HTTP status, e.g. `API error (409): ...`), invalid flags or arguments (including those `machines wait-for --help` lists under 2), and a `--wait-for-match` or `machines wait-for` timeout. `domains check` also exits 1, after printing its report, when the domain is not ready yet. |
| 2 | `domains check` only: the check was rate-limited (HTTP 429). Run it again after the printed number of seconds. |
| 1-255 (passed through) | `machines exec` and `agents exec` exit with the remote command's own status, so `if ! innstance machines exec ...` works in scripts; a status outside 1-255 becomes 1. `agents ssh` exits with the ssh client's status: the remote command's, or 255 when ssh itself fails. |
| 1-255 (a local program's) | A command that runs a program on your computer can exit with that program's status when the program fails. Today that is git: `deploy` with `[secrets]` in its config runs `git ls-files` to check the config is not committed, and a git failure other than "not tracked" (for example 128, `detected dubious ownership`) exits with git's status. The message on stderr says which check failed. |
