The DeployGPU API fails a launch in one of two places, and knowing which one you hit decides what your code should do next. A bad request never reaches a provider and never charges you: the API rejects it in the same response, with a code you can branch on. A request that looks fine can still fail later, once it is already accepted and a real provider is trying to build the machine, and that failure shows up as fields on the deployment record itself, not as an HTTP error. The rule I follow with this API is simple: never repeat a POST to /api/v1/deployments just because the first attempt looked like it failed. There is no idempotency key behind that endpoint, so a repeated call is a second, separately billed GPU, not a safe retry of the first one.
The errors you get back before anything is charged
A create call that DeployGPU rejects outright never reserves money and never touches a provider. The response is a 400, 402 or 503 with a body shaped like {"error": {"code": "...", "message": "..."}}, and the code tells you exactly what to fix.
| Code | HTTP status | What it means |
|---|---|---|
validation_error | 400 | A field is missing or malformed. The message, and the fields map, name it. |
provider_not_supported | 400 | The provider is a container provider. Vast.ai only deploys from the dashboard. |
offer_provider_mismatch | 400 | The offer_id belongs to a different provider than the provider field says. |
offer_unavailable | 400 | The offer sold out or was withdrawn since you fetched it. |
image_incompatible | 400 | The os_image_id cannot boot on this GPU, commonly an older driver image on a newer card. |
image_not_offered | 400 | The os_image_id is not in this provider's catalog for this offer. |
invalid_ssh_key | 400 | An SSH key ID is not one of yours, or the key material itself was rejected. |
insufficient_balance | 402 | Your balance will not cover one hour at the offer's rate. |
offer_cooling_down | 503 | This exact offer, provider and location just failed on capacity and is cooling down. |
A request that also attaches a volume can fail the same way, with invalid_volume, volume_unavailable, volume_provider_mismatch or volume_location_mismatch. Each of those means the volume and the offer disagree about where the instance can run.
The offer_cooling_down response carries a Retry-After header in seconds, and the same number appears as error.retry_after in the body. Read that header instead of guessing. The cooldown belongs to the offer, provider and location combination itself, not to your account, so anyone requesting that same combination gets the same response until it clears, usually in about a minute. Wait for it, then try again or move to a different offer.
The three fields on a failed deployment
Once a request is accepted, you get back {"id": "...", "status": "provisioning"} with a 202, and from here a failure is not an HTTP error anymore. Poll the deployment and, if status comes back failed, read three fields: failure_code, a stable string you can switch on, retryable, a boolean, and suggested_action, one sentence telling you what to do next. retryable describes whether trying again later, with the same request, could plausibly work. It is not a promise that DeployGPU retries anything for you, and it is not a promise that a retry succeeds, only that the failure was not caused by something wrong in the request itself.
There are eighteen values for failure_code, from PROVIDER_CAPACITY (the provider ran out of stock between your create call and the real attempt) to SSH_KEY_MISSING (only ECDSA keys were selected, and this provider takes only RSA or Ed25519) to UNKNOWN (contact support with the deployment ID). One code is easy to misread: DUPLICATE_REQUEST sounds like it comes from a client idempotency key. It does not, because there is no such key for you to send. When it shows up, the collision happened inside a provider's own internal request key, not in anything your code did. Keep the full table next to your code rather than in your head. The deployment failures reference lists every code, and the deployments endpoint reference covers the shapes around it.
Whatever the code, the reserved first hour comes back to your balance automatically, the same rule the pricing page describes for every stop. A failed launch never put a real GPU in your hands, so DeployGPU does not keep the money either.
Choosing the next offer instead of waiting on the same one
GET /api/v1/offers returns Vast.ai's container catalog in the same list as the four providers the API can actually launch on, so filter on deploymentType before you pick one. A value of virtual is a VM you can create, and dedicated is a Vast.ai container that only ever comes back provider_not_supported. Sort by pricePerGpu or priceHourly, keep the cheapest few, and try them in order when an offer turns out to be cooling down or gone.
One naming quirk is worth handling in code rather than finding out the hard way. Massed Compute's offers report their provider as the slug massed-compute, but the provider field on a create call wants the shorter key masscompute, the same string GET /api/v1/providers lists. Every other provider's slug and adapter key are the same string. Map it once and move on.
Why a timed-out create call is not safe to repeat
The safe pattern is to check before you act, not to hope. When a create call fails at the transport level rather than with a clean HTTP response, list your deployments with GET /api/v1/deployments and look for one created in the last few seconds against the provider you just tried. If you find it, use that ID. If you do not, sending the request again is safe. This is the one place in the API where the extra round trip is always worth it, because the alternative is a GPU running that nobody is using and nobody meant to pay for.
One script that ties it together
The script below combines all of this: it walks a sorted list of offers, skips container offers and ones that are cooling down or gone, checks for a stray deployment before it ever repeats a create call, and polls with a backoff instead of hammering the endpoint once a second.
import os
import time
from datetime import datetime, timezone
import requests
API = "https://api.deploygpu.ai/api/v1"
HEADERS = {"X-API-Key": os.environ["DEPLOYGPU_API_KEY"]}
# Massed Compute is the one provider whose offer slug does not match the
# key the deployments endpoint expects. Confirmed against
# GET /api/v1/providers on 2026-09-28.
PROVIDER_KEY = {"massed-compute": "masscompute"}
def list_vm_offers(gpu_slug, min_gpu_count=1):
resp = requests.get(
f"{API}/offers",
params={"gpu": gpu_slug, "min_gpu_count": min_gpu_count,
"sort_by": "priceHourly", "size": 20},
timeout=30,
)
resp.raise_for_status()
offers = resp.json()["offers"]
# "dedicated" offers are Vast.ai containers, not virtual machines.
return [o for o in offers if o["deploymentType"] == "virtual" and o["isAvailable"]]
def recent_deployment(provider_key, since):
"""A deployment for this provider created after `since`, if one exists.
Call this when a create call fails without a clean HTTP response, before
you send it again. There is no idempotency key, so a second call after a
timeout can bill a second GPU instead of finding the first one.
"""
resp = requests.get(f"{API}/deployments", headers=HEADERS,
params={"size": 25}, timeout=30)
resp.raise_for_status()
for d in resp.json()["data"]:
created = datetime.fromisoformat(d["created_at"].replace("Z", "+00:00"))
if d["provider"] == provider_key and created >= since:
return d["id"]
return None
def try_create(offer, ssh_key_ids):
provider_key = PROVIDER_KEY.get(offer["provider"]["slug"], offer["provider"]["slug"])
body = {"provider": provider_key, "offer_id": offer["id"], "ssh_key_ids": ssh_key_ids}
if offer["storageMinGB"] != offer["storageMaxGB"]:
body["disk_gb"] = offer["storageMinGB"]
attempted_at = datetime.now(timezone.utc)
try:
resp = requests.post(f"{API}/deployments", headers=HEADERS, json=body, timeout=30)
except requests.exceptions.RequestException:
# The request may or may not have reached the server. Do not repeat
# it blindly. Either it shows up below, or this offer is skipped in
# favor of the next one.
return recent_deployment(provider_key, attempted_at)
if resp.status_code == 202:
return resp.json()["id"]
error = resp.json().get("error", {})
code = error.get("code")
if resp.status_code == 503 and code == "offer_cooling_down":
wait = int(resp.headers.get("Retry-After", "60"))
print(f"{offer['provider']['name']}: cooling down for {wait}s, trying the next offer")
return None
if resp.status_code == 402:
raise RuntimeError(f"insufficient_balance: {error.get('message')}")
if code == "offer_unavailable":
print(f"offer {offer['id']} is gone, trying the next one")
return None
# validation_error, provider_not_supported, image_incompatible and the
# rest mean the request itself is wrong. Fix it before sending it again.
raise RuntimeError(f"{code}: {error.get('message')}")
def launch_cheapest(gpu_slug, ssh_key_ids):
for offer in list_vm_offers(gpu_slug):
deployment_id = try_create(offer, ssh_key_ids)
if deployment_id:
return deployment_id
raise RuntimeError(f"no offer for {gpu_slug} accepted a create call right now")
def wait_for_outcome(deployment_id, timeout_s=600):
delay, deadline = 5, time.monotonic() + timeout_s
while time.monotonic() < deadline:
resp = requests.get(f"{API}/deployments/{deployment_id}", headers=HEADERS, timeout=30)
if resp.status_code == 404:
# A 202 can be followed by a 404 for about a second before the
# row is visible for reads.
time.sleep(2)
continue
resp.raise_for_status()
deployment = resp.json()
if deployment["status"] in ("active", "failed"):
return deployment
time.sleep(delay)
delay = min(delay * 2, 30)
raise TimeoutError(f"deployment {deployment_id} did not resolve within {timeout_s}s")
if __name__ == "__main__":
deployment_id = launch_cheapest("rtx-a6000", [os.environ["DEPLOYGPU_SSH_KEY_ID"]])
result = wait_for_outcome(deployment_id)
if result["status"] == "failed":
print(result["failure_code"], result["retryable"], result["suggested_action"])
else:
print(result["status"], result["connection"])
The same flow in curl
The same shape works by hand, without the retry safety net, which is useful for checking one step at a time.
# Before creating anything, see what is already running
curl -s https://api.deploygpu.ai/api/v1/deployments \
-H "X-API-Key: $DEPLOYGPU_API_KEY"
# Create against a specific offer
curl -s -X POST https://api.deploygpu.ai/api/v1/deployments \
-H "X-API-Key: $DEPLOYGPU_API_KEY" -H "Content-Type: application/json" \
-d "{\"provider\":\"quantacloud\",\"offer_id\":\"$OFFER_ID\",\"disk_gb\":100}"
# A cooldown response looks like this
# HTTP/1.1 503 Service Unavailable
# Retry-After: 47
# {"error":{"code":"offer_cooling_down","message":"...","retry_after":47}}
# Check why a deployment failed
curl -s https://api.deploygpu.ai/api/v1/deployments/$DEPLOYMENT_ID \
-H "X-API-Key: $DEPLOYGPU_API_KEY"
Create the key itself on the API keys page in the dashboard, before any of the above.
Frequently asked questions
What does the retryable field actually promise?
Nothing about what DeployGPU will do automatically. It only says whether trying again later, with the same request, could plausibly work. DeployGPU never retries a failed launch for you.
Does a failed launch ever get billed?
No. Whatever the failure code, the reserved first hour is refunded to your balance automatically, and nothing further is charged for that attempt.
My create call timed out on my end. What should I do before sending it again?
List your deployments and look for one created in the last few seconds against the same provider. There is no idempotency key, so sending the same request again can start and bill a second GPU instead of finding the first one.
How long does a cooldown last after a capacity failure?
About a minute for that exact offer, provider and location. The response carries a Retry-After header with the real wait, so use that instead of guessing.
Is a cooldown specific to my account?
No. It applies to the offer, provider and location combination itself. Anyone requesting that same combination during the cooldown gets the same response until it clears.
Can a 400 error be fixed by sending the exact same request again?
No. A 400 means the request itself was wrong, an image that does not fit the GPU, a key that is not yours, an offer that already sold out. Change what caused it first, because the same request produces the same 400 every time.