Guides
Downloading output

Downloading output

Output is written to private object storage. Nothing we produce is publicly readable, so every download goes through a signed URL with an expiry stamped into it.

Getting a link

Reading a job returns one. outputUrl is signed at the moment you read, and expiration says when that particular link stops working.

curl https://videotranscode.cloud/api/v1/jobs/$JOB_ID \
  -H "Authorization: Bearer $VT_API_KEY"
{
  "status": "completed",
  "outputUrl": "https://sfo3.digitaloceanspaces.com/...&X-Amz-Signature=...",
  "expiration": "2026-09-17T14:12:47.118Z"
}

If you want a link without the rest of the job, GET /jobs/{id}/download-url returns just that, as downloadUrl with expiresAt.

The signature is in the query string, so downloading needs no Authorization header:

curl -o output.mp4 "$OUTPUT_URL"

How long you can reach the output

Link lifetime is how long one signed URL stays valid.

PlanLink valid for
Free4 hours
Pro7 days
Business7 days
Enterprise7 days

Seven days is a hard ceiling in the storage signer, so no link is ever valid for longer, whatever the plan.

Because links are signed at the moment you read the job, an expired link is never a problem. Read the job again and you get a new one. Do not store a signed URL in your database as if it were a permanent address; store the job identifier and sign on demand.

Storage retention is not currently enforced. Automatic deletion by plan is planned but not switched on, so output is not being removed on a schedule today. Treat that as an absence of a guarantee rather than a promise of permanence: it will be switched on, and when it is, the intended windows are about a day for Free, 7 days for Pro and 30 days for Business and Enterprise. Day-level granularity is the finest the storage lifecycle rules allow, so the free plan's 4 hours will continue to be expressed by the link lifetime rather than by deletion.

The practical answer for building against this today: the presigned URL lifetime is the access window you can rely on. Anything you need beyond it, copy to your own storage.

Download it to your own storage

Do not treat our storage as your archive. If you need the file next month, copy it to storage you control as soon as the job completes. The natural place to do that is your webhook handler.

async function onCompleted(payload) {
  const res = await fetch(payload.outputUrl)
  if (!res.ok) throw new Error(`Download failed: ${res.status}`)
 
  await uploadToYourBucket(`videos/${payload.jobId}.mp4`, res.body)
  await markReady(payload.jobId)
}

The webhook payload already carries outputUrl, so a handler does not need to call the API back before downloading.

What you get for each preset

Single-file presets, meaning the progressive MP4, social and audio ones, produce exactly one file and outputUrl points at it.

Streaming, thumbnail and sprite presets produce many files, so the output is a single ZIP archive. Unpack it before serving. See Adaptive streaming for the layout and for how to publish it.

When there is no link

outputUrl is null in four situations, and they need different responses.

SituationWhat to do
status is queued or processingWait; the job is not finished
status is failed or cancelledRead errorMessage; there is no output
Completed, but the output has been removedRun the job again
Completed, but stored under an old layoutRun the job again; the file cannot be signed

The last case only affects a small number of jobs created before the current storage layout. Asking for a link explicitly returns 410 Gone with a plain message rather than a null.