Errors
The error envelope, every status and code the API returns, what causes the common ones, and which requests are safe to retry.
Errors use standard HTTP status codes and one JSON envelope:
{
"error": {
"code": "billing.insufficient_credits",
"message": "Not enough credits: this needs 4.00 and 1.20 are available.",
"details": { "required": "4.00", "available": "1.20" },
"request_id": "59630881.7c2e4a"
}
}
Stable, machine-readable reason, named domain.reason. Branch on this.
Human-readable description, safe to show a user. The wording can change; don’t parse it.
Structured specifics, such as the field that failed or the amounts involved. Omitted when there’s nothing to add.
Identifies the request in Windpaint’s logs. Include it when you contact support. It’s only in the body; there’s no request id header.
Two responses don’t use this envelope:
- Authentication failures return
{"status": false, "error": "Unauthorized"}(401) or{"status": false, "error": "Forbidden"}(403). See Authentication. - Request timeouts return
408with{"error": "Request timeout", "message": "Request took longer than <n> seconds to process"}, where<n>is the limit that applied: 120 for uploads, a few seconds for other routes.
Check whether error is an object before reading error.code.
Status codes
| Status | code | Meaning |
|---|---|---|
400 | request.validation_failed | The body doesn’t match the endpoint’s schema: wrong type, missing required field, or unknown field. |
400 | request.bad_request | The request is malformed, for example invalid JSON or a bad query parameter. |
401 | (auth body) | Missing, invalid, revoked or expired API key. |
402 | billing.insufficient_credits | Your available credits don’t cover the job or run. |
403 | (auth body) or auth.forbidden | Your key’s role lacks the permission, or you tried to grant more access than you hold. |
404 | request.not_found, api_key.not_found, webhook.not_found, role.not_found | The resource doesn’t exist in your organization, or the route doesn’t exist. |
405 | request.method_not_allowed | The route exists but not for this HTTP method. |
408 | (timeout body) | The request took longer than the route’s time limit. The message says how long that is. |
409 | request.conflict | The request conflicts with current state: a taken slug, an archived project, the default project. |
413 | request.bad_request | The request body is too large. An upload body can be up to 51 MiB; a file over 50 MiB that fits returns 422 File exceeds 50 MiB. instead. |
422 | request.validation_failed | The body is well-formed but breaks a rule: an unsupported resolution, a missing prompt, an input asset from another project. |
422 | webhook.unknown_events, webhook.invalid_url, webhook.insecure_url | A webhook’s event names or URL were rejected. See Webhooks. |
500 | request.internal_error | Something failed on Windpaint’s side. The message is generic; quote request_id to support. |
501 | request.not_implemented | The operation exists but isn’t built for this kind of resource yet. |
503 | request.service_unavailable, billing.not_configured | A model, product or the payment provider isn’t available right now. |
A resource in another organization returns 404, the same as one that doesn’t exist.
Common errors
400: schema validation
The body failed type checking. details.errors lists each problem with the field (key) and where it was (source):
{
"error": {
"code": "request.validation_failed",
"message": "Validation failed for POST /v1/projects",
"details": {
"errors": [
{ "message": "Expected `str | null`, got `int`", "key": "slug", "source": "body" }
]
},
"request_id": "59630881.1f9a0b"
}
}
A field the endpoint doesn’t accept is reported as Object contains unknown field `foo` . Fix the request; retrying won’t help.
402: insufficient credits
{
"error": {
"code": "billing.insufficient_credits",
"message": "Not enough credits: this needs 4.00 and 1.20 are available.",
"details": { "required": "4.00", "available": "1.20" },
"request_id": "59630881.7c2e4a"
}
}
required is the job’s price (or a product run’s estimate) and available is your spendable balance, both as decimal strings. Nothing was created or charged. Top up (see Billing) or wait for running jobs to release their holds, then submit again. Credits held by running jobs aren’t available, so a burst of submits can hit 402 before your balance looks empty.
409: conflict
| Message | Cause | What to do |
|---|---|---|
A project with this slug already exists. | Creating or renaming a project to a slug that’s taken. details.slug names it. | Pick another slug. A slug derived from the name can collide too; pass slug explicitly. |
The project is archived. | Writing (submit, upload, run) into an archived project. details.project_id names it. | Use another project. Archiving can’t be undone. |
The default project cannot be archived. | Archiving the project with is_default: true. | Nothing; the default project stays. |
422: invalid inputs
422 means the request was well-formed but Windpaint can’t do what it asks. The message says exactly what’s wrong. Common ones from generation:
| Message | What to do |
|---|---|
prompt is required. | Send a non-empty prompt for capabilities that need one. |
No model implements image.edit yet. | The capability is defined but has no model. Check GET /v1/generation/capabilities for models: []. |
Unknown model 'x'. Known: ... | Use a model id from GET /v1/generation/models. The list in the message holds the same models. |
File exceeds 50 MiB. | Upload a smaller file. |
z-image-turbo resolution must be one of 1k, 2k. | Use a resolution the model lists. |
aspect_ratio must be one of 1:1, 4:3, ... | Use a listed aspect ratio. |
z-image-turbo has no quality setting. | Drop quality. No model takes it today. |
wan-2.2-i2v duration must be one of 5 seconds. | Use a listed duration, or omit it. |
video.generate on wan-2.2-i2v: slot 'start_frame' takes 1 asset(s); got 0. | Fill each input slot with the number of assets the model needs. |
Inputs must be asset ids or URLs returned by /v1/generation. | Upload external files first with POST /v1/generation/uploads and pass the asset id. |
Input asset not found in this project. | The asset is in another project, or doesn’t exist. details.asset_ids lists them. |
Input slot 'start_frame' takes image assets. | The asset is the wrong kind for that slot. |
Unsupported content type 'image/gif'. | Uploads accept PNG, JPEG, WebP, MP4, MP3 and WAV. details.allowed lists them. |
File is not a readable image. | The upload’s bytes don’t decode as the declared image type. |
Outside generation: name is required. and the slug format message on projects, A top-up must be between 100 and 100000 credits. on top-ups, and low_balance_threshold cannot be negative. on billing settings.
503: unavailable
A model that can’t run right now:
{
"error": {
"code": "request.service_unavailable",
"message": "wan-2.2-i2v is not available on this deployment.",
"details": { "model": "wan-2.2-i2v" },
"request_id": "59630881.d40c11"
}
}
The same code with "... has no price for this tier and cannot be run." means the model has no price for the resolution or duration you asked for; details names the capability, model, resolution and duration. Try another tier, or check available and prices in GET /v1/generation/models.
A product that can’t run reports why in details.reasons:
{
"error": {
"code": "request.service_unavailable",
"message": "photo-to-clip cannot run on this deployment yet.",
"details": {
"product": "photo-to-clip",
"reasons": ["..."]
},
"request_id": "59630881.e2a7f4"
}
}
Check available and unavailable_reasons on GET /v1/workflows/products before starting a run.
details only names the model. Pick another model from GET /v1/generation/models, or try again later.
On POST /v1/billing/topups, 503 means the payment provider couldn’t start checkout. Nothing was charged; try again shortly.
Failed jobs
A job that’s accepted and then fails doesn’t return an HTTP error; its status becomes failed and its error field holds one of a fixed set of messages, each ending Credits were released.:
Generation failed upstream (HTTP <status>). Credits were released.Generation failed: the model backend is unavailable. Credits were released.Generation failed: the model backend could not complete the request. Credits were released.Generation failed: the model returned no output. Credits were released.Generation failed due to an internal error. Credits were released.
A failed product run’s error is the failing step’s key followed by its reason, for example clip: Generation failed upstream (HTTP 500). Credits were released. See failure messages for what each one means.
Retrying
There are no idempotency keys, so retry decisions depend on the request:
| Request | Safe to retry? | Notes |
|---|---|---|
Any GET | Yes | Back off on 5xx and network errors. |
| Status polls | Yes | Poll at 1 to 15 second intervals. |
POST .../cancel | Yes | Canceling a finished job returns it unchanged. |
PUT /v1/billing/settings, PATCH requests | Yes | They set state; repeating gives the same result. |
DELETE requests | Yes | Deleting again returns 204. |
POST /v1/generation/estimate, product estimate | Yes | Read-only. |
POST /v1/generation/capabilities/{capability}, POST /v1/generation/models/{model} | No | A retry creates and bills a second job. |
POST /v1/workflows/products/{slug}/runs | No | A retry starts a second run. |
POST /v1/generation/uploads | Mostly | Uploads are free, but a retry creates a duplicate asset. |
POST /v1/projects, POST /v1/api-keys, POST /v1/webhooks | No | Creates a duplicate key or webhook; a duplicate project gets 409. |
POST /v1/billing/topups | Yes | Each call opens a new checkout; only completed checkouts charge. |
By status:
| Status | Retry? |
|---|---|
400, 401, 403, 404, 405, 409, 413, 422 | No. Fix the request. |
402 | After topping up or once running jobs settle. |
408, 500, network error | GETs, yes. For a submit or run, list recent jobs or runs first: the request may have gone through. |
503 | Yes, with backoff, for payment errors. For an unavailable model or product, not until it’s available. |
If a submit fails with a timeout or a dropped connection, check GET /v1/generation/requests?limit=10 (or GET /v1/workflows/runs?limit=10) for a job created in the last few seconds with your prompt before submitting again.