When something is refused, the reason arrives as a short machine-readable code. The console turns most of them into a toast in your console language; a code it does not know yet shows the API's own message, which is in English. The API returns them as JSON.
{
"detail": {
"code": "OFFER_UNAVAILABLE",
"message": "This offer is no longer available"
}
}
A few codes carry extra fields alongside code and message — DISK_TOO_LARGE returns max_disk_gb and MACHINE_DISK_EXHAUSTED returns available_disk_gb, so a client can clamp the slider and retry without a second round trip.
Those two are not interchangeable, and clamping to the wrong one loops forever. max_disk_gb is the machine's total disk; available_disk_gb is what is left after the other renters on it and a 50 GB host reserve. A machine can therefore pass DISK_TOO_LARGE and still answer MACHINE_DISK_EXHAUSTED — even an empty one, because the reserve is never sellable. Clamp to available_disk_gb when you get it.
Authentication and organization scope
| Status | Code | Cause | What to do |
|---|
| 401 | TOKEN_INVALID | No Authorization header, a malformed token, an unknown or revoked API key (removing a member revokes the personal keys they created), or an API key sent to an account endpoint | Send a valid bearer token. Account endpoints such as /v1/me and /v1/ssh-keys need a signed-in session, not an shk_ key |
| 401 | TOKEN_EXPIRED | The session token is past its expiry | Sign in again. For unattended scripts, use an API key instead |
| 403 | NOT_A_MEMBER | X-Org-Id names an organization you do not belong to | Drop the header to use your personal organization, or ask an admin for an invite |
| 403 | ORG_MISMATCH | X-Org-Id does not match the organization the API key was created in | Remove the header, or use a key belonging to that organization |
| 403 | ADMIN_REQUIRED | An admin-only action — top-up orders, 发票 details, invites, member management, team API keys — attempted as a member or with an API key | Ask an admin, or repeat the action from a signed-in admin session |
| 400 | INVALID_ORG_ID | X-Org-Id is not a valid id | Copy the id from GET /v1/orgs |
| 403 | MFA_REQUIRED | Your account has two-factor authentication enrolled, and the session token has not completed the second step | Sign in again and enter the code from your authenticator app. The console sends you to that step itself. API keys are not affected |
| 503 | AUTH_UNAVAILABLE | The API cannot check your sign-in right now — the signing keys or the two-factor state could not be read | Retry shortly. Your session is still valid |
Limits on every request
| Status | Code | Cause | What to do |
|---|
| 429 | RATE_LIMITED | Too many requests from one caller — a signed-in user, an API key, or for anonymous requests an IP address. Every /v1 request counts toward 600 a minute; POST, PUT, PATCH and DELETE also count toward 60 a minute. An IP address whose requests are refused as unauthenticated more than 60 times a minute gets this too, for every credential from it that has not already worked | Wait the seconds in the Retry-After header (also retry_after_s in the body), then retry. /v1/health and payment notifications are never limited |
| 413 | PAYLOAD_TOO_LARGE | The request body is larger than the endpoint accepts: 1 MiB for most endpoints, 4 MiB under /v1/templates. The response includes limit_bytes | Send a smaller body. A template's readme and on-start script have their own caps, well inside that |
Deploying
| Status | Code | Cause | What to do |
|---|
| 402 | INSUFFICIENT_BALANCE | Your balance does not cover one hour of the organization's burn with this instance running — its GPU and disk, plus the GPU and disk of instances running, starting or being created and the disk of stopped ones — when you deploy or start it. Being above zero is not enough. The response includes balance_micro and required_micro | Add credits. Only an admin can top up |
| 409 | OFFER_UNAVAILABLE | The last free unit of that shape was taken between the moment you opened it and the moment you deployed — or by another deploy while yours was being reserved | Go back to the marketplace and pick another offer. The count on a card is a snapshot; the deploy is what decides |
| 404 | OFFER_NOT_FOUND | GET /v1/offers/{id} was given an id that does not exist or has been withdrawn from the catalog. A deploy against an unknown offer returns OFFER_UNAVAILABLE instead | Re-list offers and use a current id |
| 422 | DISK_TOO_LARGE | The requested disk_gb is more than the machine has in total. The response includes max_disk_gb | Request no more than max_disk_gb, or choose a machine with a larger disk |
| 409 | MACHINE_IN_MAINTENANCE | The machine is booked to go offline too soon to start an instance on it. The response includes next_maintenance | Pick another machine, or come back after the window. A window further out does not block a deploy — it is shown on the offer instead |
| 409 | MACHINE_DISK_EXHAUSTED | The machine's disk is already committed to other renters. It carries several at once, and disk is sold against what is left, not against the total — so this can follow a disk_gb that cleared DISK_TOO_LARGE. The response includes available_disk_gb and requested_disk_gb | Retry at or below available_disk_gb, or pick another machine. available_disk_gb can be 0 |
| 422 | OFFER_NOT_VM_CAPABLE | A vm launch-mode template was aimed at a machine that cannot run virtual machines | Deploy the template onto a VM-capable offer, or pick a template in another launch mode |
| 404 | TEMPLATE_NOT_FOUND | The template id does not exist, or the template has been deleted | Use a template from the gallery, or a hash_id you were given |
| 404 | SSH_KEY_NOT_FOUND | The ssh_key_id is not a key this credential may attach: not one of your own, or — with a team API key — not a current member's | List GET /v1/ssh-keys with a session token and use an id from your own account. Keys belong to a user, not to an organization |
| 409 | INSTANCE_LIMIT_REACHED | The organization already has 25 instances that are not destroyed — stopped, error and still-destroying ones count. The response includes limit | Destroy instances you no longer need, including any in error. The console says "You’ve reached the instance limit for this organization. Destroy one first." |
| 422 | TEMPLATE_COMING_SOON | The template is marked coming soon: its image is not published yet — on Superheat CN, for example, the NVIDIA NGC (nvcr.io) templates | Pick another template. The console will not let you select one, so this usually reaches API callers only |
| 422 | TEMPLATE_CUDA_TOO_NEW | The template pins an older CUDA stack (cuda_max) and the machine reports a newer CUDA | Pick a machine whose CUDA is within the template's ceiling, or another template |
| 422 | TEMPLATE_ARCH_UNSUPPORTED | The template declares an architecture other than amd64/x86_64; Superheat hosts are x86_64 | Use a template with an x86_64 image |
| 422 | TEMPLATE_REQUIRES_OWNER | The template belongs to another organization and pulls from a private registry with credentials, so only the owning organization can deploy it. The console says "This template pulls from a private registry and can only be deployed by the organization that owns it." | Duplicate it into your organization and enter your own registry credentials (if you can pull that image), or ask the owner to deploy |
| 409 | OFFER_NOT_PINNABLE | The offer's machine is not registered with the fleet's control plane, so a deploy cannot be pinned to it. Listings never show such an offer; you meet this only with a stale offer_id. The console says "This machine can’t take deployments right now. Pick another one." | Re-list offers and pick another |
Every code in this section comes back before anything is kept, so a refused deploy creates nothing and costs nothing.
Instance actions
| Status | Code | Cause | What to do |
|---|
| 404 | INSTANCE_NOT_FOUND | The instance id does not exist in the organization the request is scoped to | Check the id, and check you are in the right organization |
| 409 | INVALID_STATE | The action is not legal from the instance's current state — stopping something already stopping, starting something that is running. Destroy is allowed from every state except destroyed | Wait for the instance to reach running or stopped, then retry. The instance's allowed_actions lists what is legal now. See Instance states |
| 409 | HOST_UNREACHABLE | You started a stopped instance, and the machine it lives on is not connected right now. The instance is still stopped | Start it again later |
| 409 | START_REFUSED | The host refused to resume the instance, or no longer holds its workload — the message says which. The instance is still stopped | If the host no longer holds the workload, destroy the instance and deploy again. Otherwise retry later |
| 503 | START_UNAVAILABLE | The resume could not be completed right now. The instance is still stopped; if the resume lands after all, it moves to running on its own | Retry shortly |
| 409 | CAPACITY_UNAVAILABLE | You started an instance whose GPUs were released when the balance ran out (an auto-stopped instance, or one you had stopped yourself), and the GPUs it used on that machine are now in use by someone else or out of service. The instance is still stopped, with its disk intact. The console says "The GPUs this instance used are in use by someone else right now, so it can’t start. It’s still stopped with its disk — try again later." | Start it again later: nothing can reach the disk until a start succeeds. If it keeps failing, the only way forward is to destroy the instance, which deletes the disk, and deploy again. See After the GPUs are released |
Why an instance failed
These are not refusals of a request: they are the error_code an instance carries when it lands in error, or when a stop or a start did not land and the instance went back to running or stopped. The instance page shows the explanation in your console language — under What went wrong on an errored instance, and under The last action did not complete on a running or stopped one — with the code itself underneath, so you can quote it to support. In any other state the page hides the fields, because they describe an earlier state. Over the API they are error_code and error_message on the instance.
Codes the platform sets itself. The explanation replaces the API's English error_message:
error_code | What the console says |
|---|
provision_failed | The scheduler did not accept this deploy. Nothing was billed — destroy it and try again. |
provision_lost | The deploy never reached the scheduler. Nothing was billed — destroy it and try again. |
provision_timed_out | The workload was never placed on its machine. Destroy it and try again. |
dispatch_job_missing | The scheduler no longer has this instance’s workload. Destroy it and deploy again. |
stop_timed_out | The machine did not confirm the stop. The instance is still running and billing; the stop is being retried. |
start_timed_out | The machine did not confirm the resume. It’s still stopped — try starting it again. |
workload_completed | The workload exited on its own. Its GPU has been released — destroy the instance. |
machine_unreachable | The machine this instance lives on wasn’t connected when it was started. It’s still stopped — try again later. |
start_refused | The machine refused to resume this instance. It’s still stopped. |
workload_gone | The machine no longer holds this instance’s workload. Destroy it and deploy again. |
dispatch_unavailable | The scheduler couldn’t be reached to resume this instance. It’s still stopped — try again shortly. |
gateway_unavailable | The machine couldn’t be reached to resume this instance. It’s still stopped — try again shortly. |
gpus_unavailable | The GPUs this instance used on its machine are in use by someone else right now. It’s still stopped with its disk — try starting it later, or destroy it. |
workload_gone has two sources: the machine no longer held the workload when a start reached it, or the instance's container disappeared from the machine while the instance was stopped. In the second case the instance moves to error, stops billing, and can only be destroyed.
Codes the scheduler records about the workload on its machine. The console shows the explanation and, under it as Details, the host's own error_message, which is often the exact reason — an exit status, a missing image:
error_code | What the console says |
|---|
workload_failed | The container failed on its machine. |
workload_lost | The machine lost track of the container. |
workload_exit_nonzero | The container exited with an error. |
workload_terminal | The workload had already finished on its machine. |
start_timeout | The container did not start in time. A very large image can take longer than allowed to download. |
deadline_exceeded | The workload reached its time limit. |
machine_lost | The machine went offline. |
machine_gone | The machine left the fleet. |
unknown_to_gateway | The machine has no record of this workload. |
placement_error | The workload could not be placed on a machine. |
drain | The machine was taken out of service for maintenance. |
manual_reschedule | Support moved this workload to another machine. |
image_not_provisioning_capable | This image can’t be started with this template’s launch mode. |
launch_mode_unsupported | No machine supports this template’s launch mode. |
vm_not_supported | This machine can’t run virtual machines. |
A code missing from both tables still shows, verbatim, with the message the API sent. What to do next is in Troubleshooting.
Browsing offers
| Status | Code | Cause | What to do |
|---|
| 422 | INVALID_CURSOR | The cursor on GET /v1/offers is not one the API issued, or was issued for a different sort | Pass back the next_cursor of the previous page unchanged with the same sort, or omit it to start over. See Offers |
Templates
| Status | Code | Cause | What to do |
|---|
| 403 | TEMPLATE_IMMUTABLE | You tried to edit or delete a system template curated by Superheat | Duplicate it first, then edit your copy |
| 409 | TEMPLATE_NAME_TAKEN | Your organization already has a template with that name, ignoring case | Choose a different name |
| 404 | TEMPLATE_NOT_FOUND | The id or hash_id does not resolve to a template you can see | Confirm the share link is complete and current — a hash_id changes when the launch recipe changes |
| 422 | INVALID_TAB | The tab query parameter on the template listing is not one of the gallery tabs | Use a supported tab value |
| 422 | INVALID_CURSOR | The cursor on the template listing is not one the API issued | Pass back the next_cursor of the previous page unchanged, or omit it to start over |
| 409 | PUBLIC_TEMPLATE_LIMIT_REACHED | Your organization already has 50 public templates. The response includes limit | Make one of them private, or delete one, before publishing another |
SSH keys
| Status | Code | Cause | What to do |
|---|
| 422 | INVALID_SSH_KEY | The value is not an OpenSSH public key line, the key material is not valid base64, or the type is not supported | Paste the whole contents of the .pub file, one line, starting with the key type. See Supported key types |
| 409 | DUPLICATE_SSH_KEY | You already added a key with the same fingerprint | Use the key you already have, or delete the old entry first |
| 404 | SSH_KEY_NOT_FOUND | The key id is not yours | Keys are per user. Another member's key is never visible to you |
Team and invites
| Status | Code | Cause | What to do |
|---|
| 400 | PERSONAL_ORG | You tried to invite someone into a personal organization | Create a real organization first, then invite from there |
| 404 | INVITE_NOT_FOUND | The invite token is wrong, the invite was withdrawn, or it was sent to a different email address than the one you are signed in with | Sign in with the invited address, or ask an admin for a fresh link |
| 410 | INVITE_REVOKED | Whoever sent the invite is no longer an admin of the organization, so it is no longer valid | Ask a current admin for a new invite |
| 410 | INVITE_USED | The invite has already been accepted | Ask for a new invite link |
| 410 | INVITE_EXPIRED | The invite is past its expiry | Ask for a new invite link |
| 404 | MEMBER_NOT_FOUND | The user is not a member of this organization | Refresh the team page — they may have been removed already |
| 400 | LAST_ADMIN | Removing that member, or demoting them, would leave the organization with no admin | Promote another member to admin first |
Credits
| Status | Code | Cause | What to do |
|---|
| 422 | INVALID_AMOUNT | amount_minor is neither a pack amount nor between ¥20 and ¥10,000. The response includes min_minor and max_minor in 分 (by default 2000 and 1000000) | Pick a ¥50, ¥100 or ¥500 pack, or an amount from min_minor to max_minor |
| 422 | PROVIDER_NOT_ENABLED | The payment method you asked for is not open on this site: any order before top-ups open, or wechat before the domain moves to HTTPS | Pick one from providers in GET /v1/billing/config. While that list is empty you cannot top up online |
| 503 | PROVIDER_NOT_CONFIGURED | The payment method is enabled but its credentials on the server are incomplete | A server configuration problem, not a fault in your request. Retry later, and write to finance@superheat.cloud if it persists |
| 429 | TOO_MANY_OPEN_ORDERS | You already hold five unpaid, unexpired codes | Pay one of them, or let them expire (15 minutes). Asking again for the same method and amount in the same organization (not for an invoice order) returns the code you have, as long as it has more than two minutes left. The limit of five counts your codes across all organizations |
| 422 | INVOICE_PROFILE_REQUIRED | An order was placed with want_invoice: true but the organization has no saved 发票 details | Save them first with PUT /v1/billing/invoice-profile, or switch on Request an invoice (fapiao) (开具发票) in the console and fill in the form |
| 404 | ORDER_NOT_FOUND | The order id does not exist in the organization the request is scoped to | Check the id, and check you are in the organization that placed the order |
| 403 | ADMIN_REQUIRED | A member or an API key tried to create a top-up order or to read or write the 发票 details | Ask an organization admin to add credits. See Adding credits |
| 422 | INVALID_TIME_ZONE | GET /v1/billing/spend-daily was given a tz that is not an IANA time zone name the API knows | Send a name such as Asia/Shanghai, or omit tz for UTC |
API keys
| Status | Code | Cause | What to do |
|---|
| 404 | API_KEY_NOT_FOUND | The key id does not exist in this organization | Re-list your keys and use a current id |
| 403 | ADMIN_REQUIRED | An API key tried to create or revoke another API key, or a member tried to manage a team-level key | Create and revoke keys from a signed-in session |
Requests that fail validation
A request whose body or query string does not match the schema is rejected before it reaches any of the checks above. It returns 422 with a list of field errors under detail instead of a code:
{
"detail": [
{
"type": "less_than_equal",
"loc": ["body", "disk_gb"],
"msg": "Input should be less than or equal to 20000"
}
]
}
The loc entry names the offending field. The bounds worth remembering are disk_gb between 10 and 20,000, a label of at most 64 characters, and tail on the logs endpoint between 1 and 1,000. On templates and deploys: at most 100 environment entries on the template and, counted separately, 100 in env_overrides, each value up to 4,096 characters; at most 32 ports; at most 32 constraint selectors; cuda_min, cuda_max and arch up to 32 characters; limit on the template and offer listings between 1 and 200; q on the offer listing up to 100 characters.