Skip to content

Direct Uploads

Jobs normally take a URL Xora can fetch. When your file only exists on a laptop, a CI runner, or inside a private network, direct uploads give you somewhere to put it: ask for a presigned upload, PUT the bytes, then reference the upload by id when you create the job.

POST /v1/uploads ──▶ { uploadId, url, expiresAt, maxBytes }
├── PUT <url> (raw file bytes, no auth header)
└── POST /v1/jobs { "input": { "uploadId": "…" }, … }

Uploads are transient input storage, not a media library: objects are deleted automatically 3 days after upload. Job outputs are unaffected and persist until you delete them (or land in your own bucket).

  1. Request a presigned upload

    Send the size of the file you’re about to upload. Xora checks it against your plan’s per-file limit before handing back a URL, so you find out you’re over the cap before spending time on the transfer.

    Terminal window
    curl -X POST https://api.xora.sh/v1/uploads \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "sizeBytes": 734003200,
    "filename": "interview-cam-a.mp4"
    }'

    You get back a 201 with the presigned URL:

    {
    "uploadId": "01KSD388ERH85FGR79JCW0SZZ0",
    "url": "https://xora-uploads.s3.us-east-1.amazonaws.com/uploads/user_2ab…/01KSD388…mp4?X-Amz-Signature=…",
    "expiresAt": "2026-07-24T13:00:00.000Z",
    "maxBytes": 2147483648
    }

    filename is optional and only its extension is used, to give the stored object a sensible suffix. maxBytes is the ceiling this upload will accept.

  2. PUT the file bytes

    Send the raw file as the request body — a single PUT, no Authorization header (the signature is in the URL), and no form encoding. The URL is valid for one hour.

    Terminal window
    curl -X PUT "PRESIGNED_URL_HERE" \
    --upload-file interview-cam-a.mp4

    A 200 with an ETag header means the object is stored. Browsers can PUT directly too — the bucket allows cross-origin PUT from the Xora dashboard origin, so an upload never proxies through your own server.

  3. Create a job against the upload

    Swap input.url for input.uploadId. Everything else about the job body is unchanged.

    Terminal window
    curl -X POST https://api.xora.sh/v1/jobs \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "mode": "recipe",
    "input": { "uploadId": "01KSD388ERH85FGR79JCW0SZZ0" },
    "output": { "format": "mp4" },
    "recipe": { "name": "compress", "crf": 23 }
    }'

    From here it’s an ordinary job — poll GET /v1/jobs/{id} or take a webhook, then download the output.

input.uploadId is accepted anywhere a single input.url is:

ModeSupported
mode: "recipe" (single-input recipes)Yes
mode: "probe"Yes
mode: "ffmpeg" with input / outputYes
mode: "ffmpeg" with input_files / output_filesNo — multi-input jobs take URLs
concat recipe (input_files)No — takes URLs
Running a saved presetRecipe presets: yes — pass input: { uploadId }. ffmpeg/concat presets: URL-only

input accepts exactly one of url or uploadId. Sending both, or neither, is a 400 VALIDATION_ERROR.

Max file size5 GB, or your plan’s per-file input limit if it’s lower
Upload URL validity1 hour from creation
Object retention3 days, then deleted automatically
ReuseAn upload can be used by any number of jobs until it expires
CostUploading is free — inputs are never billed. See pricing

Uploads are scoped to the account that created them: another account’s uploadId reads as not found.

Nothing is consumed by creating a job. If a job fails and you want to retry with different settings, or you want a thumbnail and an MP4 from one source, create a second job with the same uploadId — no re-upload needed, as long as the 3-day window hasn’t passed.

Direct uploads use a single PUT, which caps at 5 GB. If your plan allows larger inputs (Scale allows 6 GB), host the file and pass input.url instead. Multipart upload support is planned.

error.codeHTTPMeaning
LIMIT_EXCEEDED402The declared sizeBytes, or the file you actually uploaded, is over your plan’s per-file limit.
VALIDATION_ERROR400On /v1/uploads: sizeBytes is missing, not positive, or over 5 GB. On /v1/jobs: the upload id is unknown, expired, or the bytes were never uploaded.

The message upload not found or not yet uploaded almost always means step 2 didn’t finish — the upload record exists but the object doesn’t. Complete the PUT, then create the job.

Note that the size check at job creation reads the object Xora actually stored, not the sizeBytes you declared. Under-declaring to get a URL doesn’t get an oversized file past the plan limit; the job is refused instead.

The MCP server exposes the same flow as a create_upload tool, so an agent holding a local file can chain create_uploadPUTcreate_job without a hosting step.