---
title: "How do I upload a local image or video and use it in a workflow run?"
description: "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."
updated: 2026-10-20
---

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](/docs/api/media).

## 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.

```bash
curl -X POST https://www.wireflow.ai/api/v1/media/upload \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -F "file=@product.png"
```

```javascript
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);
```

```python
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:

```json
{
  "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:

```bash
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:

```json
{
  "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:

```bash
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.

```javascript
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);
```

```python
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.

```bash
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):

```json
{
  "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.

```bash
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"}}'
```

```javascript
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);
```

```python
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](/docs/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](/docs/api/troubleshooting).

Last updated: 2026-10-20

---

Documentation index: fetch https://www.wireflow.ai/llms.txt for the full list of Wireflow docs.
