Wait on the submit
Send Prefer: wait and the API holds the response up to 60 seconds, then returns 200 with the outputs. If the job isn't done, you get the usual 202. Good for images.
Call the REST API from your backend, or let a coding agent drive it through the hosted MCP server and agent skills. Same key, same credits, same projects.

https://api.windpaint.ai/v1Authorization: Bearer aak_…/v1/openapi.jsonhttps://mcp.windpaint.ai/mcpEvery surface is a client of the same API, so jobs, assets and credits are shared between them.
| Surface | What you get | Status |
|---|---|---|
| REST API | JSON over HTTPS at api.windpaint.ai/v1. The dashboard and every other surface are clients of it. | Available |
| MCP server | 20 tools over streamable HTTP at mcp.windpaint.ai/mcp. Works with any MCP client that can send a custom header. | Available |
| Agent skills | Five SKILL.md files plus the MCP server, packaged as a plugin for Claude Code, Codex and Cursor. | Available |
| Python client | One standard-library file you copy into your project. Not on PyPI. | Preview |
| windpaint CLI | A single static binary with JSON output and stable exit codes, for scripts and agents with a shell. | First release pending |
Send JSON with a bearer key. Add Prefer: wait and the API holds the response until the image is done, up to 60 seconds.
curl -X POST https://api.windpaint.ai/v1/generation/capabilities/image.generate \ -H "Authorization: Bearer $WINDPAINT_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=30" \ -d '{"prompt": "a lighthouse at dusk", "aspect_ratio": "16:9"}'{ "status": "queued", "request_id": "f1e2d3c4-b5a6-4978-8a6b-5c4d3e2f1a0b", "status_url": "https://api.windpaint.ai/v1/generation/requests/f1e2…/status", "cancel_url": "https://api.windpaint.ai/v1/generation/requests/f1e2…/cancel", "credits_estimate": "0.08"}| Topic | Rule |
|---|---|
| Versioning | Everything is under /v1. Additive changes ship there, so ignore fields you don't recognize. |
| Bodies | JSON, validated strictly. An unknown field returns 400 request. naming it. |
| Projects | Jobs and assets belong to a project: project_id in the body, else the X-Windpaint-Project header, else your default. |
| Ids and times | Ids are UUIDs. Timestamps are ISO 8601 in UTC. Empty fields come back as null. |
| Credits | Decimal strings such as "612.40". Parse them with a decimal type, not a float. |
| Errors | {"error": {code, message, details, request_id}}. Branch on code; quote request_id to support. |
Generation runs as jobs. Images usually finish in seconds and video in minutes, so pick the pattern that fits the wait.
Send Prefer: wait and the API holds the response up to 60 seconds, then returns 200 with the outputs. If the job isn't done, you get the usual 202. Good for images.
GET the status_url until the status is completed, failed, nsfw or canceled. Start at 1 to 2 seconds and back off to about 15.
Server-sent events from /: status, progress, log and done, with queue position. Workflow runs have their own stream.
Pass webhook_url on the submit and get a signed POST when the job starts or finishes.
curl -N https://api.windpaint.ai/v1/generation/requests/$REQUEST_ID/stream \ -H "Authorization: Bearer $WINDPAINT_API_KEY"event: statusdata: {"status":"queued","request_id":"7c9e…","queue_position":2,"progress":null, ...}event: progressdata: {"request_id":"7c9e…","progress":0.5,"queue_position":null}Browsers' EventSource can't send an Authorization header, so read the stream from your server and keep the key there.
A timeout on a submit leaves you not knowing whether the job exists. Send an idempotency key and retry with the same one.
The MCP server sends a random key on every submit, upload and workflow run, and retries the same call with it up to 3 times.
curl -X POST 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"}'# Same key, same body, within 24 hours:# the original response again, with Idempotent-Replayed: true| Situation | Response |
|---|---|
| Same key and request, first one succeeded | The original status and body, with Idempotent-Replayed: true. No new job, no new charge. |
| Same key, different body or project | 409 idempotency.. |
| Same key while the first is still processing | 409 idempotency. with Retry-After: 1. Retry with the same key. |
| Same key after the first failed | Runs as new. Failed responses aren't stored, so fix the request and retry. |
Deliveries follow the Standard Webhooks spec, so the official libraries verify them with your whsec_ secret as-is.
The webhook receiver recipe builds this out with storage, exactly-once downloads and a reconcile loop.
# pip install standardwebhooks fastapiimport osfrom fastapi import FastAPI, HTTPException, Requestfrom standardwebhooks.webhooks import Webhook, WebhookVerificationErrorwh = Webhook(os.environ["WINDPAINT_WEBHOOK_SECRET"]) # whsec_...app = FastAPI()@app.post("/hooks/windpaint")async def windpaint_hook(request: Request): raw = await request.body() # verify the raw bytes, not parsed JSON try: event = wh.verify(raw, dict(request.headers)) except WebhookVerificationError: raise HTTPException(status_code=400) # de-duplicate on request.headers["webhook-id"], act on event["data"]["status"] return {"ok": True}started and completed. completed covers every final status, not only success.webhook_url on capability and model submits, and on workflow runs.webhook-signature.webhook-timestamp more than 5 minutes from your clock.2xx within 10 seconds. Redirects aren't followed.408, 429 and 5xx.webhook-id is the same on every retry of one event.GET / v1/ webhooks/ jobs/ deliveries shows every attempt and the exact body sent.Rate limits rise with lifetime credit purchases. Every authenticated response says where you stand.
| Tier | Lifetime purchases | Concurrent jobs | Submits per minute |
|---|---|---|---|
| 1 | None (free credits only) | 5 | 60 |
| 2 | $50 or more | 10 | 120 |
| 3 | $500 or more | 25 | 300 |
| 4 | $2,000 or more | 50 | 600 |
Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over a limit you get 429 with Retry-After, and nothing is created or charged. GET /v1/billing/limits returns your tier and current usage.
Errors share one envelope with a stable error.code, such as billing.. Jobs that end failed, nsfw or canceled carry an error_code, and GET /v1/errors lists every one with whether it's worth retrying. It needs no key.
Point an agent at the hosted MCP server, or install the skills plugin and it connects the server for you. Agents read the live catalog and prices instead of guessing.
claude mcp add --transport http windpaint https://mcp.windpaint.ai/mcp \ --header "Authorization: Bearer $WINDPAINT_API_KEY"20 tools: catalog, estimate, upload, generate, wait, download and workflows. Auth is your API key in a header; there's no OAuth sign-in.
mcp.windpaint.ai/mcp
Five skills that tell the agent to read prices from the API, estimate before spending, and keep waiting on a slow job instead of resubmitting it.
github.com/windpaint-ai/skills
JSON on stdout when piped, progress on stderr, and stable exit codes an agent can branch on. For agents that work on local files.
First release pending
| Skill | Use it for |
|---|---|
windpaint | The base: auth, the catalog, estimates, async jobs, assets, projects and balance. Loaded before the others. |
windpaint-image | Text to image with image.generate, variations and seeds. |
windpaint-video | Animating a start frame into a clip with video.generate, once a video model is live. |
windpaint-workflows | Running multi-step workflows such as text-to-clip, once they're available. |
windpaint-api | Writing application code that calls the REST API. Doesn't need the MCP server. |
402 before anything runs.generate or run_workflow. Keep your client's tool approval on for those if you want to approve each spend.The OpenAPI 3.1 document is public. There's no packaged SDK yet, only a Python preview.
| Option | Status |
|---|---|
| OpenAPI 3.1 spec | Public at /v1/openapi.json, no key needed. Request bodies are complete; most response bodies are untyped, and headers like Idempotency-Key and Prefer aren't in it. Use the reference for those. |
| Python client | Preview. One file, windpaint_client.py, standard library only, Python 3.10+. Not on PyPI; ask support for the current version. It doesn't send idempotency keys or retry. |
| Other languages | No SDK. Generate a client from the spec, or call the API over HTTPS with any client. |
curl -s https://api.windpaint.ai/v1/openapi.json \ | jq '.info, (.paths | keys | length)'import windpaint_client as wp # preview, copied into your projectreq = wp.run("image.generate", prompt="a lighthouse at dusk", aspect_ratio="16:9")done = wp.wait(req["request_id"])wp.download(done["outputs"][0]["url"], "lighthouse.png")Not a packaged one yet. There is a preview Python client: a single standard-library file, not on PyPI, that you copy into your project; ask support for the current version. Any language that speaks HTTPS works, and the public OpenAPI 3.1 spec can generate a client, though most response bodies in it are untyped.
No. There are no publishable or browser-safe keys, and a key can spend your organization's credits. Call Windpaint from your backend and hand the client a signed asset URL, which is valid for 15 minutes.
Send an Idempotency-Key header on submits, uploads and workflow runs, and reuse it when you retry. Within 24 hours, the same key and request return the original response instead of creating and billing a second job.
Every route lives under /v1. There is no version header and no dated versions; additive changes such as new fields, endpoints and enum values ship into /v1, so ignore fields you don't recognize.
On tier 1 an organization can have 5 jobs in flight and make 60 submits per minute, and each API key can make 1,200 requests per minute. The tier rises with lifetime credit purchases. A request over a limit gets 429 with Retry-After, and nothing is created or charged.
No. The hosted server takes your API key in the Authorization header and stores nothing. Clients that can only connect through OAuth can't use it yet.
No. A job's price is held from your balance when you submit it, and jobs that end failed, nsfw or canceled release the hold in full. You pay only for completed outputs.
Request early access. We’ll email you when your account is approved, with 200 free credits to spend in the first 30 days.