# Uploads

> Upload images, video and audio to Nodaro with POST /v1/upload, copy files from a URL, import social videos, trim stored media for free and list your library.

Source: https://nodaro.ai/docs/developers/api/uploads

**Uploads** put your own images, video and audio into Nodaro's storage, so that any node can use them by URL. `POST /v1/upload` stores one file and returns its URL. Other routes copy a file from a URL, import a video from a social network, cut a stored file for free, or list the media you have stored.

## Endpoints

| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/v1/upload` | Upload one file, as multipart form data. |
| `POST` | `/v1/save-to-storage` | Copy a file from a URL into your storage, as a job. |
| `POST` | `/v1/download-video` | Import a video from a social network or a direct video link. |
| `GET` | `/v1/download-video/progress/:downloadId` | The import's progress, as server-sent events. |
| `POST` | `/v1/video-metadata` | Read a video's duration, size and title without downloading it. |
| `POST` | `/v1/media/process` | Trim or crop a stored video or audio file. Free and synchronous. |
| `GET` | `/v1/library` | Your stored media, with a storage summary. |

## Upload a file

Send the file as multipart form data in the `file` field. You can also send `type`, set to `image`, `video` or `audio`.

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/upload \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -F "file=@portrait.png;type=image/png" \
  -F "type=image"
```

```json
{
"data": {
"url": "https://…/portrait.png",
"assetId": "5d6e7f80-91a2-4b3c-8d4e-5f60718293a4",
"thumbnailUrl": "https://…/portrait-thumb.webp",
"category": "image",
"filename": "portrait.png",
"mimeType": "image/png",
"sizeBytes": 1482233,
"r2Key": "…/portrait.png"
}
}
```

**TypeScript SDK**

```ts
// In a browser, file comes from an <input type="file">.
// In Node 20 or newer, build it from bytes:

const file = new File([await readFile('portrait.png')], 'portrait.png', { type: 'image/png' })
const upload = await client.uploads.upload(file)
console.log(upload.url) // pass it as imageUrl, videoUrl or audioUrl
```

| Field | Meaning |
| --- | --- |
| `url` | The stored file's public URL. Use it in any node. |
| `assetId` | The id of the file in your storage. |
| `thumbnailUrl` | A thumbnail for images and video, or `null` for audio. |
| `category` | `image`, `video` or `audio`, as the server classified the file. |
| `filename` | The display name of the file. |
| `mimeType` | The file type the server settled on. See below. |
| `sizeBytes` | The stored size in bytes. |
| `r2Key` | The file's key in storage. |

The CLI has no upload command. To copy a file that is already online into your storage from the terminal, use `nodaro media save <url>`.

### Formats and file types

| Media | Formats |
| --- | --- |
| Image | PNG, JPEG, WebP |
| Video | MP4, MOV, WebM |
| Audio | MP3, WAV, M4A and AAC, OGG, WebM, FLAC. Up to 50 MB. |

Each media type has its own size limit. The type your client declares does not have to be the standard one, because the server works out the real type before it checks the file:

- **Parameters are removed.** `audio/webm;codecs=opus` becomes `audio/webm`.
- **Common alternative spellings are mapped.** `audio/vnd.dlna.adts`, which Windows uses for a plain `.aac` file, becomes `audio/aac`, `image/jpg` becomes `image/jpeg`, and `video/mov` becomes `video/quicktime`.
- **An unhelpful type is read from the file name.** With `application/octet-stream`, or no type at all, the extension decides: `clip.mp4` is stored as `video/mp4`.

A type that still matches no accepted format answers `400 validation_error`, with the list of accepted formats. The type the server settled on comes back as `mimeType`, and the file is served with it.

### When an upload is refused

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `validation_error` | The file is not in an accepted format. |
| 413 | | Your storage is full. The SDK throws `StorageExceededError`, with your limit in `limitBytes`. |
| 422 | `upload_blocked` | The deployment's upload policy refused the file before it was stored. Show the `message` to your user as it is. Only on deployments with such a policy. |

## Use a URL instead

Most routes take media by URL, such as `imageUrl`, `videoUrl`, `audioUrl` and `referenceImageUrls`, so a file that is already online does not need an upload. The URL must be a public `http` or `https` address: a URL that points at a private network address, or uses another scheme, is refused.

Copy the file into Nodaro's storage when the URL is temporary, such as a signed link that expires, or when you want the file to stay available:

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/save-to-storage \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mediaUrl": "https://example.com/clip.mp4", "filename": "clip.mp4", "mediaType": "video"}'
# {"jobId":"…"}  poll the job; its output carries the stored file
```

**TypeScript SDK**

```ts
const { jobId } = await client.media.saveToStorage({
mediaUrl: 'https://example.com/clip.mp4',
filename: 'clip.mp4',
mediaType: 'video',
})
```

**CLI**

```bash
nodaro media save https://example.com/clip.mp4 --filename clip.mp4 --type video --watch
```

In a workflow, pass an uploaded file's URL to an [Upload Image](https://nodaro.ai/docs/nodes/image/upload-image), [Upload Video](https://nodaro.ai/docs/nodes/video/upload-video) or [Upload Audio](https://nodaro.ai/docs/nodes/audio/upload-audio) node through the run's inputs. The media `url` fields of input nodes can always be overridden by a run. See [Run a workflow with new input values](https://nodaro.ai/docs/developers/api/workflows#run-a-workflow-with-new-input-values).

## Import a video from a social network

`POST /v1/download-video` imports a video from YouTube, TikTok, Instagram, X or Facebook, or from a direct link to a video file, into your storage. It answers with a `downloadId`, not a job id:

```json
{ "url": "https://youtu.be/XXXX", "maxHeight": 720, "sectionStartSec": 30, "sectionEndSec": 90 }
```

- `maxHeight` limits the resolution; leave it out for the best available. `sectionStartSec` and `sectionEndSec` fetch only that part of the video: send both or neither.
- A video without a sound track fails, unless you send `requireAudio: false`.
- Each account can run 4 imports at once. A fifth answers `429 too_many_downloads`.
- `GET /v1/download-video/progress/:downloadId` streams the progress as server-sent events, about twice a second: `{ phase, percent, videoUrl?, thumbnailUrl?, error? }`. The stream ends when the import is `completed`, with the stored `videoUrl`, or `failed`, with an `error`. The progress is kept only for a short time after the import ends, so start reading it right away.
- The finished video is saved in your library.

To check a video before you import it, `POST /v1/video-metadata` with `{ url }` reads its duration, size and title without downloading it. From the terminal: `nodaro media download <url> --max-height 720 --watch` and `nodaro media metadata <url>`. The other media tools are in [Voice and media](https://nodaro.ai/docs/developers/api/voice-and-media).

## Trim or crop a stored file

`POST /v1/media/process` cuts or crops a stored video or audio file. It is free and answers at once, without a job:

```json
{
"sourceUrl": "https://…/interview.mp4",
"type": "video",
"trim": { "startTime": 12.5, "endTime": 48 },
"crop": { "x": 0, "y": 0, "width": 1080, "height": 1080 }
}
```

The answer is `{ "data": { url, thumbnailUrl, assetId, sizeBytes, mimeType, metadata } }`. `format` sets the output format: `mp4` or `webm` for video, `mp3`, `wav`, `m4a` or `aac` for audio. With `deleteSource: true`, the source file is removed afterwards when it is yours and nothing else uses it, so the cut replaces the original. In the SDK, call `client.media.process(input)`. For trimming inside a workflow, use the [Trim Video](https://nodaro.ai/docs/nodes/video/trim-video) node, which costs credits.

## List your stored media

`GET /v1/library` returns your stored images, video and audio, with a summary of your storage. Filter it with `type` (`image`, `video` or `audio`) and page through it with `limit` and `cursor`. In the SDK, call `client.library.list({ type: 'image' })`.

## Upload from an AI assistant

The [MCP server](https://nodaro.ai/docs/mcp/tools) offers three ways to upload, each for images, audio and video. They all need the `assets:write` scope.

| Way | Tools | Works in |
| --- | --- | --- |
| In-chat file picker | `upload_image_widget`, `upload_audio_widget`, `upload_video_widget` | Claude.ai on the web. The image picker takes up to 10 files. |
| Browser upload page | `request_image_upload`, `request_audio_upload`, `request_video_upload` | Every MCP client. The tool returns a page to open in a browser and the file's final public URL. |
| Presigned upload URL | `prepare_image_upload`, `prepare_audio_upload`, `prepare_video_upload` | Clients that can run shell commands, such as Claude Code, Cursor or Cline. Send the file with `curl -X PUT --data-binary @file`. Not on Claude.ai web or Android. |

## Frequently asked questions

### How do I upload a file to Nodaro through the API?

Send POST /v1/upload as multipart form data with the file in the file field. The response carries the stored file's url, which you can pass to any node as imageUrl, videoUrl or audioUrl.

### Which file formats can I upload?

Images in PNG, JPEG or WebP, video in MP4, MOV or WebM, and audio in MP3, WAV, M4A or AAC, OGG, WebM or FLAC. Audio files can be up to 50 MB.

### Do I have to upload a file before a node can use it?

No. Most routes take a public http or https URL directly. Upload the file, or copy it with POST /v1/save-to-storage, when the URL is private or temporary, or when you want Nodaro to keep its own copy.

### What happens when my storage is full?

The upload answers 413, and the SDK throws StorageExceededError with your limit in limitBytes. Delete files you no longer need, or free space, then upload again.

### How do AI assistants upload files through MCP?

The MCP server offers three ways, an in-chat file picker, a browser upload page, and a presigned upload URL for clients that can run shell commands. Each exists for images, audio and video.
