Windpaint
Overview

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:

EndpointsShapeExample
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}/cancelBare object{"status": "queued", "request_id": "...", ...}
Webhooks: /v1/webhooks/*Named wrapper{"webhook": {...}}, {"webhooks": [...], "total": 3}, {"events": [...]}
Everything elsedata 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 null rather than left out.

Pagination

List endpoints use one of three styles:

StyleEndpointsParametersResponse
Limit onlyGET /v1/generation/requests, GET /v1/generation/assets, GET /v1/workflows/runs, GET /v1/billing/holds, GET /v1/billing/topupslimitThe newest limit items
Limit and offsetGET /v1/webhooks, GET /v1/billing/ledgerlimit, offsetPage plus total
CursorGET /v1/runslimit, cursorPage 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"
X-Windpaint-Projectstring

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.

Next steps