NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
REST API

Uploads

Upload images, video and audio with POST /v1/upload, copy files from a URL, import videos from links, trim stored media, search your library and link to a file.

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

MethodPathWhat it does
POST/v1/uploadUpload one file, as multipart form data.
POST/v1/upload-jsonStore a JSON document, up to 25 MB, and get its public URL. For example, an image editor's design state for an edited image. The URL is hard to guess but public, so never store a secret in it.
POST/v1/save-to-storageCopy a file from a URL into your storage, as a job.
POST/v1/download-videoImport a video from a social network or a direct video link.
GET/v1/download-video/progress/:downloadIdThe import's progress, as server-sent events.
POST/v1/video-metadataRead a video's duration, size and title without downloading it.
POST/v1/media/processTrim or crop a stored video or audio file. Free and synchronous.
POST/v1/media/import-urlCopy a picture from a web address into your storage.
POST/v1/media/discardThrow away a file you just made and no longer need.
GET/v1/libraryYour media library, newest first, with search and filters.
POST/v1/library/save-generatedSave one of your files to your library.

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 -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"
{
  "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"
  }
}
// In a browser, file comes from an <input type="file">.
// In Node 20 or newer, build it from bytes:
import { readFile } from 'node:fs/promises'

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
FieldMeaning
urlThe stored file's public URL. Use it in any node.
assetIdThe id of the file in your storage.
thumbnailUrlA thumbnail for images and video, or null for audio.
categoryimage, video or audio, as the server classified the file.
filenameThe display name of the file.
mimeTypeThe file type the server settled on. See below.
sizeBytesThe stored size in bytes.
r2KeyThe 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

MediaFormats
ImagePNG, JPEG, WebP, GIF, AVIF, HEIC, HEIF. Up to 25 MB.
VideoMP4, MOV, WebM, AVI. Up to 500 MB.
AudioMP3, 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.

HEIC, HEIF and AVIF images are converted before they are stored. Few browsers show these formats, and the image models do not accept them. Every image upload lane stores such a file as a JPEG, or as a PNG when the picture has transparency, and mimeType reports the stored type. The lanes are POST /v1/upload, POST /v1/upload/image, the upload behind the MCP tools prepare_image_upload and request_image_upload, and the server-side import of an image URL. A lane that gave out the file's URL before the upload keeps that URL, and only its content type changes. This covers 10-bit AVIF files and iPhone HEIC photos. These two are read by a decoder that does not keep transparency, so a transparent area in them comes out opaque. An 8-bit AVIF keeps its transparency. A file that cannot be decoded at all is refused with 400.

MP4 and MOV videos are stored with their index at the front. A recording from OBS or a phone often keeps this index at the end of the file, so the first seek into a large file is slow. Every video upload lane rewrites such a file once, before it is stored, by copying its streams into a new container. Nothing is re-encoded: the picture and the sound stay identical. A file that already has its index at the front, or that the rewrite cannot verify, is stored exactly as sent. Because a rewritten file is wrapped again, the stored size can differ slightly from the bytes you sent. sizeBytes is always the stored file's length, so do not compare it to your local file size or checksum.

When an upload is refused

StatusCodeMeaning
400validation_errorThe file is not in an accepted format, or an image or audio file is over its size limit.
413storage_limit_exceededYour storage is full. The SDK throws StorageExceededError, with the code in reason and your usage and limit in details: usedBytes, quotaBytes, remainingBytes and tier.
422upload_blockedThe 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 -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
const { jobId } = await client.media.saveToStorage({
  mediaUrl: 'https://example.com/clip.mp4',
  filename: 'clip.mp4',
  mediaType: 'video',
})
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, Upload Video or 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.

Import a video from a social network

POST /v1/download-video imports the video behind a link to a video's page, or from a direct link to a video file, into your storage. Send the content rights confirmation for the link as confirmationId (see Content rights). It answers with a downloadId, not a job id:

{ "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, with about 3 seconds more on each side, because the cut lands on keyframes. Send both or neither, and trim the result when you need an exact cut. With exactSection: true, the part is cut at exactly that range instead, with no margin.
  • 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.

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:

{
  "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 node, which costs credits.

Import a picture from a web address

POST /v1/media/import-url with the body { "url": "https://…" } copies a picture from a web address into your storage. The address must be http or https and not private, and the picture can be up to 20 MB. The platform keeps the file only if it decodes as an image: JPEG, PNG, WebP or GIF. A HEIC, HEIF or AVIF picture is converted to JPEG or PNG. The stored copy is an upload of yours, served so that a browser canvas can read its pixels, for example to crop it.

The answer is { "data": { url, thumbnailUrl, assetId, mimeType, sizeBytes, filename, width, height } }. A refused import answers 400 validation_error for an address or a format that is not accepted, 422 fetch_failed when the address cannot be reached, and 413 when the picture is over 20 MB (file_too_large) or your storage is full (storage_limit_exceeded). In the SDK, call client.uploads.importUrl(url). The Upload Image node uses this import when you paste an image link.

Throw away a file you no longer need

POST /v1/media/discard with the body { "url": "https://…" } throws away a file you just made and no longer need. Examples are a link download you replaced before using it, a cut replaced by a newer one, and a picture you imported and then cancelled. It is not a general delete. The file is removed only when it is your own and was made by /v1/download-video, /v1/media/process or a URL import. It must also be less than 24 hours old, and no job of yours may use it, as an output or as an input such as a run's videoUrl.

AnswerMeaning
{ "data": { "discarded": true } }The file was removed.
{ "data": { "discarded": false } }There was nothing of yours at that address: the file is already gone, or it is not yours.
409 not_discardableThe file does not meet the conditions above. Nothing was touched.

In the SDK, call client.media.discard({ url }).

Your media library

GET /v1/library lists your media, newest first. By default it lists the files you saved to your library plus the shared library. With owned=true, it lists every file you own instead: uploads and generations, saved or not.

QueryMeaning
typeall (the default), image, video or audio.
qSearch: only the files whose filename contains every word of q, up to 5 words, ignoring case. Every character is matched as itself, so %, _ and * are not wildcards. A blank q is no search, and q takes up to 200 characters. The earlier name, search, still works.
ownedtrue lists every file you own instead of the library view.
sourceOnly media from one surface: internal, mcp, app, cli, sdk, extension, web or api. Files from before this was recorded have no source, so they appear only when source is left out.
limitPage size, 1 to 100. The default is 40.
cursorThe nextCursor of the previous page.

The answer is { data, nextCursor, totalCount }. Each item carries id, type, filename, mimeType, sizeBytes, url, thumbnailUrl, metadata, isLibraryItem, uploadSource, source, sourceDetail, jobId and createdAt. jobId is the job that made the file, or null for an upload. nextCursor is null on the last page, and totalCount, the number of matching files, comes with the first page only. A library row whose stored file is gone is not listed and not counted, so every item has a url.

curl -s "https://app.nodaro.ai/v1/library?owned=true&type=image&q=poster" \
  -H "Authorization: Bearer $NODARO_API_KEY"

In the SDK, call client.library.list({ type: 'image', q: 'poster' }). The MCP tool browse_uploads reads the owned=true view, with the same filename search as its search argument.

POST /v1/library/save-generated with { url, type } saves a file to your library. A file on Nodaro storage is accepted only when it is yours: one of your generations, a file you uploaded, or a file in your upload folder. Any other file on Nodaro storage answers 403 forbidden, whether or not it exists. A link to any other site is saved as given.

To send a signed-in user to one file in the app's file library, My Files, link to /library with one of two query parameters:

LinkOpens
https://app.nodaro.ai/library?job=<jobId>The file that job made: the jobId your generation call returned.
https://app.nodaro.ai/library?asset=<assetId>That file: an id from GET /v1/library?owned=true. Without owned=true the list also holds shared library items that My Files never shows.
  • The page searches, then shows the file. It shows "Looking for the file…", then scrolls to the file, marks it and opens its preview.
  • It looks through about the user's newest 1,000 files. A link to an older file opens My Files without it.
  • When the file cannot be shown, a short message says why, and the page opens as usual: the id is unknown or not the user's, the job is still running, or the job failed or made no file. When the lookup itself fails, the message asks the user to try the link again.
  • The parameter is removed from the address once read, so a refresh or Back does not open the file again.
  • A user who is not signed in signs in first and then lands on the file. A self-hosted install answers the same paths on its own address.

Upload from an AI assistant

The MCP server offers three ways to upload, each for images, audio and video. They all need the assets:write scope.

WayToolsWorks in
In-chat file pickerupload_image_widget, upload_audio_widget, upload_video_widgetClaude.ai on the web. The image picker takes up to 10 files.
Browser upload pagerequest_image_upload, request_audio_upload, request_video_uploadEvery MCP client. The tool returns a page to open in a browser and the file's final public URL.
Presigned upload URLprepare_image_upload, prepare_audio_upload, prepare_video_uploadClients 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, WebP, GIF, AVIF, HEIC or HEIF, video in MP4, MOV, WebM or AVI, and audio in MP3, WAV, M4A or AAC, OGG, WebM or FLAC. Images can be up to 25 MB, video up to 500 MB and audio 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 details.quotaBytes. Delete files you no longer need, or free space, then upload again.

How do I link a user to one file in their Nodaro library?

Link to https://app.nodaro.ai/library?job= followed by the job id, or ?asset= followed by an id from GET /v1/library?owned=true. The page finds the file, scrolls to it and opens its preview.

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.

Last updated on

On this page