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.
| Content type | Stored as kind |
|---|---|
image/png, image/jpeg, image/webp | image |
video/mp4 | video |
audio/mpeg (MP3), audio/wav | audio |
- 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/jpgis accepted asimage/jpeg. Anything else returns422with the allowed list indetails.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-Projectheader, 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"
}
}
The asset id. Use it in a job’s inputs and with the asset endpoints.
The project the asset belongs to.
For outputs, the request_id of the job that produced it. null for uploads.
The asset’s API URL, .../assets/{id}/content. Fetching it needs your key. It also works in place of the id in inputs.
image, mask, video, audio or text. Today you’ll see image, video and audio.
upload or output.
MIME type.
File size.
Pixel width for images and video.
Pixel height for images and video.
Length in seconds for generated video. null for uploads, which aren’t probed for duration.
The uploaded filename. null for outputs.
When the asset was stored (ISO 8601).
List and get assets
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.
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 return422withInputs 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
422withInput asset not found in this project.and the offending ids indetails.asset_ids. - Right kind. Each slot accepts certain kinds (
start_frametakesimage). A wrong kind returns422naming the slot. - Right count. Each slot has a minimum and maximum;
GET /v1/generation/capabilitieslists 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.