Process user-uploaded video in the cloud
Users upload MOV, HEVC, huge phone files and broken files. Send each one to Xora, and get back an MP4 that plays everywhere, a thumbnail, and a webhook when it is done.
Uploads are the hard part of video
Any app that takes video from users gets every kind of file: iPhone HEVC, 4K screen recordings, MOV files from cameras, files with no audio, and files that are not video at all. Browsers play only some of them, and a 2 GB upload is not something to serve as is.
Running FFmpeg on your own servers for this means a queue, workers, disk space and a way to isolate untrusted files. Xora runs the FFmpeg part. Your app sends a URL and gets a webhook.
- User uploads to your storage
- probe → is it a video? how long?
- webReady → MP4 that plays everywhere
- thumbnail → poster image
The pipeline
Three API calls per upload. Each job is independent, so a failed thumbnail does not block the video.
-
Check the file
Run
mode: "probe"on the upload URL. ReadhasVideo,durationSeconds,width,heightandvideoCodec, and reject files that do not fit your rules before you spend anything on an encode. -
Make it play everywhere
Run the
webReadyrecipe. It passes through files that already play in browsers, remuxes when only the container or index must change, and re-encodes HEVC, ProRes and other formats to H.264 MP4. AddscaleDown: trueto limit the size. -
Add a poster image
Run the
thumbnailrecipe withseekSecondsandstageInput: false. When the storage supports range requests, only the part of the file around that time is read.
From your upload handler
Send the jobs when the upload lands in your storage. Use the upload ID in each idempotencyKey, so a retry of your handler does not start the same job twice. A presigned GET URL from S3 or R2 works as the input.
When a job ends, Xora sends a POST to webhookUrl with data.jobId, data.state and, for a completed job, data.output.signedUrl. Save the URL, or let the output land in your own R2 bucket.
For files that are on your server and have no URL, use a direct upload: POST /v1/uploads, one PUT, then pass the uploadId to the job.
const XORA = 'https://api.xora.sh/v1/jobs'const headers = { Authorization: `Bearer ${process.env.XORA_API_KEY}`, 'Content-Type': 'application/json',}const webhookUrl = 'https://your-app.com/webhooks/xora'
async function processUpload(uploadId, fileUrl) { const jobs = [ { mode: 'probe', input: { url: fileUrl } }, { mode: 'recipe', input: { url: fileUrl }, output: { format: 'mp4' }, recipe: { name: 'webReady', scaleDown: true, height: 1080 }, }, { mode: 'recipe', input: { url: fileUrl }, output: { format: 'jpg' }, recipe: { name: 'thumbnail', seekSeconds: 1 }, stageInput: false, }, ] const names = ['probe', 'web', 'thumb'] return Promise.all( jobs.map((job, i) => fetch(XORA, { method: 'POST', headers, body: JSON.stringify({ ...job, webhookUrl, idempotencyKey: `${uploadId}_${names[i]}` }), }).then((r) => r.json()), ), )}Limits and cost
The same rules apply to every job, so you can plan for them.
| Plan | Max input file | Jobs at once | Credits each month |
|---|---|---|---|
| Free | 250 MB | 2 | 25 |
| Build | 2 GB | 10 | 400 |
| Scale | 6 GB | 50 | 1500 |
Credits are billed for machine time and output size. A probe or a thumbnail uses a small part of a credit, and an upload that is already web-ready is passed through without an encode. Failed, rejected and cancelled jobs cost nothing. See pricing for worked examples.
More detail: ffprobe API · web-ready video settings · thumbnails from a URL
Frequently asked questions
How do I process user-uploaded video with FFmpeg?
Let the user upload to your storage, then send the file URL to an FFmpeg API. With Xora, run a probe job to check the file, a webReady job to make an MP4 that plays in every browser, and a thumbnail job for the poster image. A webhook tells your app when each one is done.
What happens if a user uploads a file that is not a video?
The probe job tells you what the file contains, with fields such as hasVideo, durationSeconds and videoCodec. If the file has no video stream or cannot be read, reject it before you run an encode. Failed and rejected Xora jobs cost nothing.
How large can an upload be?
The input limit depends on the Xora plan, from 250 MB on Free to 6 GB on Scale. Check the size in your own upload step as well, so users get a clear message before they upload.
Do I need to re-encode every upload?
No. The webReady recipe probes the file first. It returns files that already play in browsers as they are, remuxes files that only need a container or index change, and re-encodes only the rest.
Can the processed files go to my own storage?
Yes. On paid plans, connect a Cloudflare R2 bucket once, and every output is written to it. On every plan, outputs are also available as signed URLs.
Handle your first upload today
Start on the Free plan with no card. Send one upload URL and get a web-ready MP4 and a thumbnail back.