Idempotency
Send an Idempotency-Key header on submits, uploads and workflow runs so a retry after a timeout returns the original response instead of creating and billing a second job.
A network error or timeout on a submit leaves you not knowing whether the job was created. Send an Idempotency-Key header and you can retry the same request safely: if the first one went through, the retry returns its response instead of creating, and billing, a second job.
curl https://api.windpaint.ai/v1/generation/capabilities/image.generate \
-H "Authorization: Bearer $WINDPAINT_API_KEY" \
-H "Idempotency-Key: 4f1c2a9e-order-123" \
-H "Content-Type: application/json" \
-d '{"prompt": "a fox in the snow"}'
Sending the same request again with the same key returns the same request_id, with the response header Idempotent-Replayed: true.
Any string from 1 to 255 characters, compared exactly (case-sensitive). Optional; without it, every request runs as new. A UUID per logical operation works well.
Endpoints
| Endpoint | Response that’s replayed |
|---|---|
POST /v1/generation/capabilities/{capability} | 202, bare submit body |
POST /v1/generation/models/{model} | 202, bare submit body |
POST /v1/generation/uploads | 201, {"data": asset} |
POST /v1/workflows/{slug}/runs | 202, {"data": run} |
Every other endpoint ignores the header.
How keys are matched
- Scope. A key belongs to your organization and one endpoint group. The capability endpoint, the model endpoint, uploads and workflow runs are four separate groups, so the same key value can be used once in each. Keys aren’t tied to an API key or user: any credential in your organization that sends the same key and the same request gets the stored response. Other organizations’ keys never collide with yours.
- Lifetime. A key is kept for 24 hours from the first request that used it. After that it’s free again, and a request with it runs as new.
- Fingerprint. Along with the key, Windpaint stores a fingerprint of the request: method, path, the project it resolved to (from
project_id, theX-Windpaint-Projectheader, or the default), and the JSON body. Key order and whitespace in the body don’t matter. For uploads the fingerprint is the file’s SHA-256, its filename and its content type.
What happens on a repeat
| Situation | Response |
|---|---|
| First request with a key | Runs normally. A 2xx response is stored. |
| Same key and same request, after the first succeeded | The original status code and body, byte for byte, with Idempotent-Replayed: true. No new job, run or asset is created, and no credits are held or charged. |
| Same key, different request (another body, project, capability or workflow) | 409 idempotency.key_reused |
| Same key while the first request is still being processed | 409 idempotency.in_progress with Retry-After: 1. Retry with the same key. |
Same key after the first request failed with a 4xx or 5xx | Runs as new. Failed responses aren’t stored, so fix the request and retry with the same key. |
| Empty key, or longer than 255 characters | 422 idempotency.key_invalid |
If the server stops partway through a request, the key counts as in progress for up to 5 minutes; after that, a retry with the same key runs again.
A 5xx returned after the job was already committed (for example, the job was saved but couldn’t be queued) releases the key. A retry with the same key can then create a second job. Before retrying a 5xx on a submit, check GET /v1/generation/requests for the job.
Errors
All three use the standard error envelope.
| Status | Code | details |
|---|---|---|
409 | idempotency.key_reused | {"header": "Idempotency-Key"} |
409 | idempotency.in_progress | {"retry_after": 1} |
422 | idempotency.key_invalid | {"header": "Idempotency-Key", "max_length": 255} |
A retry loop
Generate the key once per logical operation, before the first attempt, and reuse it on every retry of that operation:
import time, uuid, httpx
def submit(body: dict) -> dict:
key = str(uuid.uuid4())
for attempt in range(5):
try:
r = httpx.post(
"https://api.windpaint.ai/v1/generation/capabilities/image.generate",
headers={"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": key},
json=body,
timeout=60,
)
except httpx.TransportError:
time.sleep(2 ** attempt)
continue
if r.status_code == 409 and r.json()["error"]["code"] == "idempotency.in_progress":
time.sleep(int(r.headers.get("Retry-After", "1")))
continue
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
r.raise_for_status()
return r.json()
raise RuntimeError("submit did not succeed after 5 attempts")
Clients
The windpaint CLI and the MCP server send a random Idempotency-Key on capability submits, uploads and workflow runs. They retry the same call up to 3 times, one second apart, with the same key on network errors and idempotency.in_progress.