Introduction
Base URL, versioning, response envelopes, pagination and the other conventions every Windpaint endpoint follows.
The Windpaint API is a JSON-over-HTTPS API for generating images and video, managing the assets they produce, and running your account. The dashboard, the CLI and the MCP server all use it.
curl https://api.windpaint.ai/v1/generation/capabilities \
-H "Authorization: Bearer $WINDPAINT_API_KEY"
Base URL and versioning
https://api.windpaint.ai/v1
Every route lives under /v1. There’s no version header and no way to pin a dated version; additive changes (new fields, new endpoints, new enum values) ship into /v1, so ignore fields you don’t recognize.
Authentication
Send an API key as a bearer token on every request:
Authorization: Bearer aak_...
See Authentication for permissions and failure responses.
Requests
Send JSON bodies with Content-Type: application/json. The one exception is POST /v1/generation/uploads, which takes multipart/form-data.
Request bodies are validated strictly. A field with the wrong type, a missing required field, or a field the endpoint doesn’t know returns 400 request.validation_failed with the field named in details. Don’t send extra fields. See Errors.
Response envelopes
Responses don’t share one envelope. Which one you get depends on the endpoint group:
| Endpoints | Shape | Example |
|---|---|---|
Generation submit, status and cancel: POST /v1/generation/capabilities/{capability}, POST /v1/generation/models/{model}, GET /v1/generation/requests/{id}/status, POST /v1/generation/requests/{id}/cancel | Bare object | {"status": "queued", "request_id": "...", ...} |
Webhooks: /v1/webhooks/* | Named wrapper | {"webhook": {...}}, {"webhooks": [...], "total": 3}, {"events": [...]} |
| Everything else | data wrapper | {"data": {...}} or {"data": [...]} |
Two data endpoints add fields next to data: GET /v1/billing/ledger returns {"data": [...], "total": 120}. GET /v1/runs puts its page inside data: {"data": {"items": [...], "total": 120, "next_cursor": "..."}}.
DELETE endpoints return 204 No Content with an empty body. GET /v1/generation/assets/{id}/content returns a 302 redirect to the file.
Errors always use the error envelope {"error": {...}}, except authentication failures. See Errors.
Data types
- Ids are UUID strings.
- Credits are decimal strings, such as
"0.08"or"612.40". Parse them with a decimal type, not a float. - Timestamps are ISO 8601 in UTC, such as
"2026-10-04T14:12:09.481220Z". Send timestamps the same way; include an offset. - Nulls are explicit. Response fields with no value are returned as
nullrather than left out.
Pagination
List endpoints use one of three styles:
| Style | Endpoints | Parameters | Response |
|---|---|---|---|
| Limit only | GET /v1/generation/requests, GET /v1/generation/assets, GET /v1/workflows/runs, GET /v1/billing/holds, GET /v1/billing/topups | limit | The newest limit items |
| Limit and offset | GET /v1/webhooks, GET /v1/billing/ledger | limit, offset | Page plus total |
| Cursor | GET /v1/runs | limit, cursor | Page plus total and next_cursor |
GET /v1/projects, GET /v1/api-keys, GET /v1/billing/usage, GET /v1/generation/capabilities, GET /v1/generation/models and GET /v1/workflows/products return everything in one response.
Out-of-range limit values are clamped rather than rejected. Generation and product-run lists default to 50 and cap at 200; the billing ledger and holds default to 100 and cap at 500. Lists are newest first.
To walk a cursor list, pass the previous response’s next_cursor as cursor until next_cursor is null. For an offset list, keep going while offset + limit < total.
Project scoping
Jobs, assets and product runs belong to a project. A request picks its project from project_id in the body, then the X-Windpaint-Project header (an id or a slug), then your organization’s default project:
curl "https://api.windpaint.ai/v1/generation/assets?limit=20" \
-H "Authorization: Bearer $WINDPAINT_API_KEY" \
-H "X-Windpaint-Project: campaign-q4"
Project id or slug. Applies to generation, product and run endpoints. An unknown project returns 404; writing into an archived project returns 409.
See Organizations and projects.
Asynchronous operations
Generation is always asynchronous. Submitting a job or starting a product run returns 202 Accepted with the job in queued status and a URL to poll:
{
"status": "queued",
"request_id": "f1e2d3c4-b5a6-4978-8a6b-5c4d3e2f1a0b",
"status_url": "https://api.windpaint.ai/v1/generation/requests/f1e2d3c4-b5a6-4978-8a6b-5c4d3e2f1a0b/status",
"cancel_url": "https://api.windpaint.ai/v1/generation/requests/f1e2d3c4-b5a6-4978-8a6b-5c4d3e2f1a0b/cancel",
"credits_estimate": "0.08"
}
Poll status_url until the status is completed, failed, nsfw or canceled. Start at 1 to 2 seconds and back off to about 15 seconds; images usually finish in seconds and video in minutes. There’s no synchronous mode. See Requests.
Idempotency
There are no idempotency keys. Retrying a POST that submits a job or starts a run creates, and bills, a second one. If a submit times out or the connection drops, list recent jobs with GET /v1/generation/requests (or runs with GET /v1/workflows/runs) before you resubmit. The Errors page lists which requests are safe to retry.
Rate limits
The API doesn’t rate-limit requests today, and responses carry no rate-limit headers. Your credit balance is the practical limit on generation. Limits may be added later; handle 429 with backoff if you see one.
Response headers
Every response includes X-Process-Time: the server time spent on the request, in milliseconds. There’s no request id header; the error body carries request_id instead.