POST /upload accepts one or more files and an optional list of transformations. When transformations are provided, Openinary generates and caches each variant immediately — so the first user request is served from cache instead of triggering on-demand processing.
For the full endpoint reference — all parameters, form fields, and response schema — see the Upload Files API reference .
Video transformations run in a background queue — they don’t block the upload response.
curl -X POST "http://localhost:3000/upload" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "files=@./video.mp4;type=video/mp4" \
-F 'transformations=["w_1280,h_720,q_75","w_640,h_360,q_60"]'
Even without a transformations field, Openinary automatically queues a default thumbnail for every video upload.
TypeScript examples
Image upload
Video upload
Multiple fields
import { readFile } from "node:fs/promises" ;
const apiKey = process . env . OPENINARY_API_KEY ;
if ( ! apiKey ) throw new Error ( "Missing OPENINARY_API_KEY" );
const bytes = await readFile ( "./photo.jpg" );
const file = new File ([ bytes ], "photo.jpg" , { type: "image/jpeg" });
const form = new FormData ();
form . append ( "files" , file );
form . append (
"transformations" ,
JSON . stringify ([ "w_800,h_600,f_webp,q_80" , "w_400,h_400,c_fill,f_avif,q_70" ]),
);
const res = await fetch ( "http://localhost:3000/upload" , {
method: "POST" ,
headers: { Authorization: `Bearer ${ apiKey } ` },
body: form ,
});
if ( ! res . ok ) throw new Error ( `Upload failed: ${ res . status } ${ await res . text () } ` );
const data = await res . json ();
console . log ( data );
import { readFile } from "node:fs/promises" ;
const apiKey = process . env . OPENINARY_API_KEY ;
if ( ! apiKey ) throw new Error ( "Missing OPENINARY_API_KEY" );
const bytes = await readFile ( "./video.mp4" );
const file = new File ([ bytes ], "video.mp4" , { type: "video/mp4" });
const form = new FormData ();
form . append ( "files" , file );
form . append (
"transformations" ,
JSON . stringify ([ "w_1280,h_720,q_75" , "w_640,h_360,q_60" ]),
);
const res = await fetch ( "http://localhost:3000/upload" , {
method: "POST" ,
headers: { Authorization: `Bearer ${ apiKey } ` },
body: form ,
});
if ( ! res . ok ) throw new Error ( `Upload failed: ${ res . status } ${ await res . text () } ` );
const data = await res . json ();
console . log ( data );
import { readFile } from "node:fs/promises" ;
const apiKey = process . env . OPENINARY_API_KEY ;
if ( ! apiKey ) throw new Error ( "Missing OPENINARY_API_KEY" );
const bytes = await readFile ( "./photo.jpg" );
const file = new File ([ bytes ], "photo.jpg" , { type: "image/jpeg" });
const form = new FormData ();
form . append ( "files" , file );
// Each append is one transformation string
form . append ( "transformations" , "w_1200,h_800,f_webp,q_82" );
form . append ( "transformations" , "w_600,h_600,c_fill,f_avif,q_70" );
const res = await fetch ( "http://localhost:3000/upload" , {
method: "POST" ,
headers: { Authorization: `Bearer ${ apiKey } ` },
body: form ,
});
if ( ! res . ok ) throw new Error ( `Upload failed: ${ res . status } ${ await res . text () } ` );
const data = await res . json ();
console . log ( data );
Limits
Max 20 transformations per upload request.
Max file size defaults to 50 MB . Configurable via MAX_FILE_SIZE_MB — useful for large video uploads in self-hosted setups.
Tips
Prewarm only the variants you actually serve — don’t prewarm speculatively.
Use consistent transformation strings across your frontend and backend to maximize cache hits.
For video, monitor the queue via GET /queue/stats during high-volume ingestion.
Next steps
Upload API reference Full endpoint reference: all fields, validation, and response schema.
Image Transformations Full parameter reference for resizing, cropping, and format conversion.
Video Transformations Resize, trim, and extract thumbnails from video files.