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.
Endpoint
POST /upload
Authorization : Bearer <API_KEY>
Content-Type : multipart/form-data
Upload with prewarmed variants
curl -X POST "http://localhost:3000/upload" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "files=@./photo.jpg;type=image/jpeg" \
-F 'transformations=["w_800,h_600,f_webp,q_80","w_400,h_400,c_fill,f_avif,q_70"]'
The transformations field accepts:
A JSON array of transformation strings
Multiple transformations form fields (one string per field)
A newline- or semicolon-separated string
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 );
Response
{
"success" : true ,
"files" : [
{
"filename" : "photo.jpg" ,
"path" : "uploads/photo.jpg" ,
"size" : 248731 ,
"url" : "/t/uploads/photo.jpg" ,
"prewarmedUrls" : [ "/t/w_800,h_600,f_webp,q_80/uploads/photo.jpg" ]
}
]
}
Field Type Description filenamestring Original filename pathstring Storage path sizenumber File size in bytes urlstring Base transformation URL prewarmedUrlsstring[] Cached variants (images) prewarmErrorsobject[] Prewarm failures per variant (images) queuedTransformationUrlsstring[] Queued variants (videos) queueErrorsobject[] Queue failures per variant (videos)
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
Image Transformations Full parameter reference for resizing, cropping, and format conversion.
Video Transformations Resize, trim, and extract thumbnails from video files.