Media Uploads
Get a local file onto the Wireflow CDN so you can wire it into a workflow — inline for small files, presigned direct-to-R2 for anything up to 25MB.
Workflows consume media by URL. These endpoints put your file on cdn.wireflow.ai, which is already on the executor's host allowlist, so the returned url drops straight into an input:image / input:video / input:audio node or any media port.
All three doors return the same response shape, so pick one by file size and parse the result the same way:
{
"url": "https://cdn.wireflow.ai/uploads/api/42/a1b2....png",
"mediaId": "a1b2....png",
"contentType": "image/png",
"width": 1024,
"height": 1024,
"duration": null,
"bytes": 812345
}
Which door to use
| Your file | Use | Limit |
|---|---|---|
| Small, as a multipart file | POST /media/upload with file=@... |
4MB |
Small, as a base64 dataUrl |
POST /media/upload with { "dataUrl" } |
3MB |
| Up to 25MB, bytes in hand | POST /media/upload-url → PUT → /complete |
25MB |
| Already hosted at a public URL | POST /media/upload with { "url": "..." } |
25MB |
Why 4MB and not 25MB inline? The API runs on serverless functions that reject a request body over roughly 4.5MB before any Wireflow code runs. Rather than let you discover that as an opaque platform error, the inline door refuses at 4MB and points you here. The presigned flow has no such ceiling because the bytes go straight from you to storage.
And why 3MB for a
dataUrl? Same 4MB limit, different units. base64 puts about 4/3 of the file on the wire, and the limit is measured on the encoded body, so adataUrltops out around 3MB of actual file. A 3.6MB image is under "4MB" and still too big to send this way — use multipart, or the presigned flow.
Scope for every endpoint on this page: workflows:write. Uploads are listed by GET /media.
Inline upload
POST /api/v1/media/upload
Send one of:
multipart/form-datawith afilefield, up to 4MB- JSON
{ "dataUrl": "data:image/png;base64,..." }, up to 3MB of file (see above) - JSON
{ "url": "https://example.com/clip.mp4" }— we fetch and rehost it, up to 25MB
curl -X POST https://wireflow.ai/api/v1/media/upload \
-H "Authorization: Bearer $WIREFLOW_API_KEY" \
-F "[email protected]"
Optional width, height and duration fields are accepted as hints. They are only used when we cannot read the value out of the file itself, and they never override what we measured.
Presigned upload (up to 25MB)
Three calls. Finish within 24 hours: an upload that never reaches step 3 is swept, along with its object, by a daily cleanup. The asset does not appear in GET /media until step 3 either.
1. Ask for a URL. bytes is the exact byte length of the file.
curl -X POST https://wireflow.ai/api/v1/media/upload-url \
-H "Authorization: Bearer $WIREFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contentType":"video/mp4","bytes":18874368,"filename":"broll.mp4"}'
{
"uploadUrl": "https://....r2.cloudflarestorage.com/...?X-Amz-Signature=...",
"method": "PUT",
"headers": {
"Content-Type": "video/mp4",
"Content-Length": "18874368",
"Cache-Control": "public, max-age=31536000, immutable"
},
"mediaId": "a1b2c3d4-....mp4",
"url": "https://cdn.wireflow.ai/uploads/api/42/a1b2c3d4-....mp4",
"contentType": "video/mp4",
"bytes": 18874368,
"expiresAt": "2026-08-18T12:15:00.000Z"
}
2. PUT the bytes to uploadUrl with exactly the headers you were given. The content type and the byte length are part of the signature, so a mismatch is rejected by storage with SignatureDoesNotMatch. The URL is valid for 15 minutes.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
-H "Content-Length: 18874368" \
--data-binary @broll.mp4
3. Finalize.
curl -X POST https://wireflow.ai/api/v1/media/upload-url/complete \
-H "Authorization: Bearer $WIREFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mediaId":"a1b2c3d4-....mp4"}'
This verifies the object landed, checks its size, confirms an image is really a decodable image, and returns the standard response above. It is safe to retry.
If it answers unreadable_image, the bytes you PUT were not the image type you asked for. The object is discarded — mint a new upload url and send the real file.
Metadata on the presigned path
Images get their width and height read from the file. Video and audio return null for width, height and duration unless you send them yourself on the /complete call — we will not download a 25MB file just to measure it. Sent values are bounded and are only ever used to fill a gap.
Accepted types
Images jpg, png, gif, webp, avif · video mp4, webm, mov · audio mp3, wav, m4a.
Anything else is refused with unsupported_type.
Errors
| Code | Meaning |
|---|---|
unsupported_type |
The content type is not in the list above |
file_too_large |
Over the limit for the door you used |
invalid_bytes |
bytes is missing or is not a positive whole number |
invalid_media_id |
The mediaId is not one we minted |
upload_not_found |
Unknown mediaId, the PUT never happened, or the 24h window lapsed |
unreadable_image |
The bytes are not a decodable image of the type you declared |
empty_file |
Zero bytes |