How do I upload a local image or video and use it in a workflow run?

Upload a local image or video with your API key, get its URL, and pass that URL as a workflow run input: up to 4MB in one call, or 100MB for video in 3 steps.

Last updated View as Markdown

To use a local image or video in a workflow run, upload it to Wireflow first, then pass the url the upload returns as the value of the workflow's image, video or audio input in POST /api/v1/workflows/{id}/run. Files up to 4MB go in one call to POST /api/v1/media/upload, and bigger files use a three-step upload URL flow that takes up to 25MB, or 100MB for video.

You need:

  • An API key with the workflows:write scope for uploads and workflows:execute for runs, or Full access. A new key starts read-only. Create it in Settings > API Keys: it starts with sk- and is shown only once. The examples read it from the WIREFLOW_API_KEY environment variable.
  • A workflow you own with an input that takes a file. /run works only on workflows you own.

Which file types and sizes can I upload?

You can upload images (jpg, png, gif, webp, avif), video (mp4, webm, mov) and audio (mp3, wav, m4a): up to 4MB in a single call, and up to 25MB, or 100MB for video, through an upload URL.

Your file How to send it Largest file
Small, from disk POST /api/v1/media/upload as a multipart form with a file field 4MB
Small, as a base64 dataUrl POST /api/v1/media/upload with JSON { "dataUrl": "..." } 3MB
Image or audio, from disk POST /api/v1/media/upload-url, then a PUT, then /complete 25MB
Video, from disk POST /api/v1/media/upload-url, then a PUT, then /complete 100MB
Already online at a public URL POST /api/v1/media/upload with JSON { "url": "..." } 25MB

SVG, the dataUrl limit and the rest are in Media Uploads.

How do I upload a small file and get its URL?

Send the file as a multipart form to POST /api/v1/media/upload. The reply carries the file's url on cdn.wireflow.ai, ready to pass into a workflow input.

curl -X POST https://www.wireflow.ai/api/v1/media/upload \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -F "file=@product.png"
import { readFile } from 'node:fs/promises';

const API = 'https://www.wireflow.ai/api/v1';
const auth = { Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}` };

const form = new FormData();
form.append('file', new Blob([await readFile('product.png')], { type: 'image/png' }), 'product.png');

const res = await fetch(`${API}/media/upload`, { method: 'POST', headers: auth, body: form });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const media = await res.json();
console.log(media.url);
import os
import requests

API = "https://www.wireflow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"}

with open("product.png", "rb") as f:
    res = requests.post(
        f"{API}/media/upload",
        headers=HEADERS,
        files={"file": ("product.png", f, "image/png")},
    )
res.raise_for_status()
media = res.json()
print(media["url"])

The reply:

{
  "url": "https://cdn.wireflow.ai/uploads/api/42/a1b2....png",
  "mediaId": "a1b2....png",
  "contentType": "image/png",
  "width": 1024,
  "height": 1024,
  "duration": null,
  "bytes": 812345
}

Every upload method returns this same shape, so you can read url the same way whichever one you use.

To copy a file that is already online, send JSON instead of a form. Wireflow fetches the file and hosts its own copy, up to 25MB:

curl -X POST https://www.wireflow.ai/api/v1/media/upload \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/clip.mp4"}'

You can also send width, height and duration as hints. They are used only when Wireflow can't read the value from the file, and they never override what it measured.

How do I upload a video or a file larger than 4MB?

Use the three-step upload URL flow: ask POST /api/v1/media/upload-url for a signed upload URL, PUT the bytes to it with exactly the headers it gives you, then call POST /api/v1/media/upload-url/complete to finish and get the file's url.

1. Ask for an upload URL. Send the file's contentType, its exact length in bytes and its filename. A file over the limit for its type, 25MB or 100MB for video, is refused here with file_too_large, before anything is signed.

2. PUT the bytes within 15 minutes. Send them to uploadUrl with exactly the headers from step 1. The content type and the byte length are part of the signature, so a mismatch is rejected with SignatureDoesNotMatch.

3. Finish the upload. Send the mediaId to /complete. Wireflow checks that the file arrived, checks its size against the same limit, and checks that an image can be decoded, then returns the same reply as a small upload. This call is safe to retry.

Finish within 24 hours: an upload that never reaches step 3 is removed by a daily cleanup, and it doesn't appear in GET /api/v1/media until step 3. For video and audio, width, height and duration come back null on this path unless you send them yourself on the /complete call.

The step 1 reply, with the signed URL shortened:

{
  "uploadUrl": "https://...",
  "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"
}

The shell version saves that reply, then sends every header in it on the PUT:

FILE=broll.mp4
BYTES=$(wc -c < "$FILE" | tr -d ' ')

# 1. Ask for an upload URL
curl -s -X POST https://www.wireflow.ai/api/v1/media/upload-url \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"contentType\":\"video/mp4\",\"bytes\":$BYTES,\"filename\":\"$FILE\"}" \
  -o upload.json

# 2. PUT the bytes with exactly the headers you were given
jq -r '.headers | to_entries[] | "\(.key): \(.value)"' upload.json > put-headers.txt
curl -X PUT "$(jq -r '.uploadUrl' upload.json)" \
  -H @put-headers.txt \
  --data-binary @"$FILE"

# 3. Finish the upload
curl -s -X POST https://www.wireflow.ai/api/v1/media/upload-url/complete \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"mediaId\":\"$(jq -r '.mediaId' upload.json)\"}"

If upload.json holds an error code instead of an uploadUrl, find the code in the table at the end of this page.

import { readFile } from 'node:fs/promises';

const API = 'https://www.wireflow.ai/api/v1';
const auth = { Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}` };

async function post(path, body) {
  const res = await fetch(`${API}${path}`, {
    method: 'POST',
    headers: { ...auth, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}

const bytes = await readFile('broll.mp4');

// 1. Ask for an upload URL
const upload = await post('/media/upload-url', {
  contentType: 'video/mp4',
  bytes: bytes.length,
  filename: 'broll.mp4',
});

// 2. PUT the bytes with exactly the headers you were given
const put = await fetch(upload.uploadUrl, {
  method: upload.method,
  headers: upload.headers,
  body: bytes,
});
if (!put.ok) throw new Error(`PUT ${put.status}: ${await put.text()}`);

// 3. Finish the upload
const media = await post('/media/upload-url/complete', { mediaId: upload.mediaId });
console.log(media.url);
import os
import requests

API = "https://www.wireflow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"}
FILE = "broll.mp4"

# 1. Ask for an upload URL
res = requests.post(
    f"{API}/media/upload-url",
    headers=HEADERS,
    json={"contentType": "video/mp4", "bytes": os.path.getsize(FILE), "filename": FILE},
)
res.raise_for_status()
upload = res.json()

# 2. PUT the bytes with exactly the headers you were given
with open(FILE, "rb") as f:
    put = requests.request(upload["method"], upload["uploadUrl"], headers=upload["headers"], data=f)
put.raise_for_status()

# 3. Finish the upload
res = requests.post(
    f"{API}/media/upload-url/complete",
    headers=HEADERS,
    json={"mediaId": upload["mediaId"]},
)
res.raise_for_status()
media = res.json()
print(media["url"])

How do I pass the uploaded file into a workflow input?

Put the file's url in inputs, under the id of the input it should fill, and send it to POST /api/v1/workflows/{id}/run.

To find the id, GET the same /run URL. Inputs that take a file report type image, audio, video, or url for any media URL. Go by type, not by the node's name: Wireflow's Image Input node also carries video and sound.

curl https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $WIREFLOW_API_KEY"

Response 200 OK (trimmed, with example values):

{
  "data": {
    "workflowId": "cm1abc123",
    "workflowName": "Product clip",
    "inputs": [
      { "nodeId": "node-clip", "label": "Source clip", "type": "video", "required": true },
      { "nodeId": "node-prompt", "label": "Edit prompt", "type": "text", "required": false, "default": "slow push-in, warm light" }
    ],
    "endpoint": "/api/v1/workflows/cm1abc123/run",
    "method": "POST"
  }
}

Then start the run with the uploaded file. The JavaScript and Python versions continue from the upload code above.

curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"inputs": {"node-clip": "https://cdn.wireflow.ai/uploads/api/42/a1b2c3d4-....mp4"}}'
const run = await fetch(`${API}/workflows/YOUR_WORKFLOW_ID/run`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({ inputs: { 'node-clip': media.url } }),
});
if (!run.ok) throw new Error(`${run.status}: ${await run.text()}`);
const { data } = await run.json();
console.log(data.executionId, data.poll);
res = requests.post(
    f"{API}/workflows/YOUR_WORKFLOW_ID/run",
    headers=HEADERS,
    json={"inputs": {"node-clip": media["url"]}},
)
res.raise_for_status()
data = res.json()["data"]
print(data["executionId"], data["poll"])

The reply is 202 Accepted with an executionId and a poll path. To confirm the upload itself, GET /api/v1/media lists your uploads. To follow the run, see check when a run is finished.

What can go wrong with an upload?

A refused upload returns a code that names the problem, and the fix depends on the code:

Code What it means What to do
unsupported_type The content type isn't one of the accepted types Send jpg, png, gif, webp, avif, mp4, webm, mov, mp3, wav or m4a
file_too_large The file is over the limit for the method and file type you used Over 4MB, use the upload URL flow. Over 25MB, or 100MB for video, no upload method takes it
empty_file The file has zero bytes Check the path and send the real file
invalid_bytes bytes is missing or isn't a positive whole number Send the file's exact length in bytes
SignatureDoesNotMatch Storage rejected the PUT because its headers differ from the signed ones Send exactly the headers from step 1
invalid_media_id The mediaId isn't one Wireflow issued Send the mediaId from step 1
upload_not_found Unknown mediaId, the PUT never happened, or the 24 hours ran out Start again from step 1
unreadable_image The bytes aren't a decodable image of the type you declared, and the upload is discarded Ask for a new upload URL and send the real file
invalid_svg Sent as SVG, but the root element isn't svg Send a real SVG file
svg_too_complex The SVG has more than 20,000 elements or is nested more than 64 deep Simplify the SVG
upload_busy (429) Another upload URL request for your account kept its turn for about a second Retry after Retry-After (1 second). Nothing was signed
upload_budget_exceeded (429) More than 10GB of upload URLs were requested in the last hour Wait the seconds in Retry-After. The message says when room comes back

An upload URL is valid for 15 minutes, so if the PUT starts later, ask for a new one. Errors outside uploads, such as 401 or 402, are covered in why an API request fails.

For AI agents: the full documentation index is at /llms.txt, and most docs pages are available as Markdown by adding .md to the URL.

© 2026 Wireflow. All rights reserved.