Windpaint
Generation

Assets

Upload files, list and read assets, download their bytes, and use them as inputs to generation jobs.

An asset is a stored media file in a project: either something you uploaded or the output of a generation job. Jobs read their inputs from assets and write their outputs as assets, so the same id that comes back from one job can be passed to the next.

curl -X POST https://api.windpaint.ai/v1/generation/uploads \
  -H "Authorization: Bearer $WINDPAINT_API_KEY" \
  -F "[email protected]"

Upload a file

POST /v1/generation/uploads takes a multipart form with the file in a field named data, and returns 201 with the new asset.

curl
curl -X POST https://api.windpaint.ai/v1/generation/uploads \
  -H "Authorization: Bearer $WINDPAINT_API_KEY" \
  -H "X-Windpaint-Project: launch-video" \
  -F "[email protected];type=image/png"
Content typeStored as kind
image/png, image/jpeg, image/webpimage
video/mp4video
audio/mpeg (MP3), audio/wavaudio
  • Size: up to 50 MiB per file. Empty files are rejected.
  • Type: taken from the part’s Content-Type, or guessed from the filename when it’s missing. image/jpg is accepted as image/jpeg. Anything else returns 422 with the allowed list in details.allowed.
  • Images must decode. Their width and height are read and stored; a file that isn’t a readable image returns 422.
  • Cost: uploads are free. You only pay for generation.
  • Project: the asset goes into the project picked by the X-Windpaint-Project header, or your default project. Upload into the project you’ll run the job in, since inputs must come from the same project.

The asset object

{
  "data": {
    "id": "8e1f2a3b-4c5d-4e6f-9a0b-1c2d3e4f5a6b",
    "project_id": "5b0c7e1a-2f3d-4e5f-8a9b-0c1d2e3f4a5b",
    "job_id": null,
    "url": "https://api.windpaint.ai/v1/generation/assets/8e1f2a3b-4c5d-4e6f-9a0b-1c2d3e4f5a6b/content",
    "kind": "image",
    "source": "upload",
    "content_type": "image/png",
    "size_bytes": 422193,
    "width": 1024,
    "height": 576,
    "duration_s": null,
    "filename": "frame.png",
    "created_at": "2026-10-04T14:01:02.551Z"
  }
}
idstring

The asset id. Use it in a job’s inputs and with the asset endpoints.

project_idstring

The project the asset belongs to.

job_idstring | null

For outputs, the request_id of the job that produced it. null for uploads.

urlstring

The asset’s API URL, .../assets/{id}/content. Fetching it needs your key. It also works in place of the id in inputs.

kindstring

image, mask, video, audio or text. Today you’ll see image, video and audio.

sourcestring

upload or output.

content_typestring

MIME type.

size_bytesinteger

File size.

widthinteger | null

Pixel width for images and video.

heightinteger | null

Pixel height for images and video.

duration_snumber | null

Length in seconds for generated video. null for uploads, which aren’t probed for duration.

filenamestring | null

The uploaded filename. null for outputs.

created_atstring

When the asset was stored (ISO 8601).

List and get assets

curl
# Assets in the current project, newest first
curl "https://api.windpaint.ai/v1/generation/assets?limit=50" \
  -H "Authorization: Bearer $WINDPAINT_API_KEY"

# One asset
curl https://api.windpaint.ai/v1/generation/assets/8e1f2a3b-4c5d-4e6f-9a0b-1c2d3e4f5a6b \
  -H "Authorization: Bearer $WINDPAINT_API_KEY"

GET /v1/generation/assets returns {"data": [Asset]} for the current project, uploads and outputs together, newest first. limit takes 1 to 200 (default 50); there’s no cursor or offset and no filter by kind, so filter on kind or source client-side. GET /v1/generation/assets/{id} returns {"data": Asset}.

Read an asset’s bytes

GET /v1/generation/assets/{id}/content (the asset’s url) checks your key and answers with a 302 redirect to a signed download URL that’s valid for 15 minutes. Follow the redirect to get the file.

curl
curl -L https://api.windpaint.ai/v1/generation/assets/$ASSET_ID/content \
  -H "Authorization: Bearer $WINDPAINT_API_KEY" \
  -o output.png

Don’t send your Authorization header to the redirect target. The signed URL carries its own credentials and will reject a request that adds yours. curl -L, Python requests and httpx drop the header automatically when a redirect goes to a different host; if you follow redirects by hand, make the second request without it.

Get a URL for a browser

A browser can’t send your API key, and you shouldn’t give it one. To show or download an asset in a web page, fetch the signed URL on your server and hand that to the browser:

curl https://api.windpaint.ai/v1/generation/assets/$ASSET_ID/url \
  -H "Authorization: Bearer $WINDPAINT_API_KEY"
{ "data": { "url": "https://...signed...", "expires_in": 900 } }

The URL works without any header for expires_in seconds (15 minutes) and can go straight into an <img>, <video> or a download link. Fetch a new one when it expires; don’t store it. For anything longer-lived, download the bytes and serve them from your own storage. Media in your app has a full example.

Use assets as inputs

Pass assets to a job in inputs, keyed by slot name:

{
  "prompt": "slow dolly in",
  "inputs": { "start_frame": ["8e1f2a3b-4c5d-4e6f-9a0b-1c2d3e4f5a6b"] }
}

The rules:

  • Ids or API URLs only. Each value is an asset id or an asset URL returned by this API (https://api.windpaint.ai/v1/generation/assets/{id}/content). External URLs aren’t fetched; they return 422 with Inputs must be asset ids or URLs returned by /v1/generation. Upload the file first.
  • Same project. The asset must be in the project the job runs in. An asset from another project, or an id that doesn’t exist, returns 422 with Input asset not found in this project. and the offending ids in details.asset_ids.
  • Right kind. Each slot accepts certain kinds (start_frame takes image). A wrong kind returns 422 naming the slot.
  • Right count. Each slot has a minimum and maximum; GET /v1/generation/capabilities lists them per model.

Outputs are inputs too: any completed job’s outputs[].id can be passed to the next job in the same project. That’s how you go from an image to a video.

Reading across projects

Reads and use are scoped differently:

  • Reading (GET /assets/{id}, /content, /url) works for any asset in your organization, whatever project header you send, so an asset URL keeps working wherever it’s pasted inside your org.
  • Listing (GET /assets) shows only the current project.
  • Using as an input works only within the asset’s own project.

To use an asset from project A in a job in project B, download it and upload it into B.

Retention

Assets don’t expire, and there’s no endpoint to delete one today. Everything you upload or generate stays in its project.