Upload API

Upload a video without the dashboard: create an upload, send the file straight to storage, confirm it, then poll until it's ready.

Every request needs your API key in the Authorization header. See Authentication for how to create one.

1. Create the upload

POST /api/v1/uploads takes a filename and returns a video id plus a presigned upload for your source file. This call does not upload any bytes; it just reserves the video and gives you a place to send it.

shellRequest
curl -X POST https://api.vdox.example.com/api/v1/uploads \
  -H "Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e" \
  -H "Content-Type: application/json" \
  -d '{"filename": "lecture.mp4"}'

Response, 201 Created:

jsonResponse
{
  "videoId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "upload": {
    "url": "https://vdox-uploads.s3.ap-south-1.amazonaws.com/",
    "fields": {
      "bucket": "vdox-uploads",
      "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
      "X-Amz-Credential": "AKIAEXAMPLE/20260713/ap-south-1/s3/aws4_request",
      "X-Amz-Date": "20260713T090000Z",
      "key": "01H8Y3ZK5XG5W3E5V8N7Q2R4T6/01ARZ3NDEKTSV4RRFFQ69G5FAV/original.mp4",
      "Policy": "eyJleHhwaXJhdGlvbiI6IjIwMjYtMDctMTNUMDk6MzA6MDBaIiwiY29uZGl0aW9ucyI6W119",
      "X-Amz-Signature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
    }
  }
}

upload.fields is an S3 presigned POST: every key in it is a form field your upload request must include. The exact keys and values are generated per request, use whatever upload.fields actually returned, not the example above verbatim.

2. Upload the file to storage

POST the file to upload.url as multipart/form-data. This goes straight to storage, not to vdoX. Two rules S3 enforces on presigned POSTs:

  • Send every field from upload.fields as its own -F flag, in the order they appear in the response.
  • Add the file itself last, as a field named file. It has to come after every field from upload.fields, or S3 rejects the request.

With the example fields above, that looks like:

shellTerminal
curl -X POST "https://vdox-uploads.s3.ap-south-1.amazonaws.com/" \
  -F "bucket=vdox-uploads" \
  -F "X-Amz-Algorithm=AWS4-HMAC-SHA256" \
  -F "X-Amz-Credential=AKIAEXAMPLE/20260713/ap-south-1/s3/aws4_request" \
  -F "X-Amz-Date=20260713T090000Z" \
  -F "key=01H8Y3ZK5XG5W3E5V8N7Q2R4T6/01ARZ3NDEKTSV4RRFFQ69G5FAV/original.mp4" \
  -F "Policy=eyJleHhwaXJhdGlvbiI6IjIwMjYtMDctMTNUMDk6MzA6MDBaIiwiY29uZGl0aW9ucyI6W119" \
  -F "X-Amz-Signature=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
  -F "Content-Type=video/mp4" \
  -F "file=@lecture.mp4"
# The first seven -F flags are upload.fields verbatim. Content-Type is NOT in
# upload.fields: you add it yourself (the policy requires video/*), and file
# must be the last field.

Since the fields differ on every call, script it instead of hardcoding field names:

shellupload.sh
UPLOAD=$(curl -s -X POST https://api.vdox.example.com/api/v1/uploads \
  -H "Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e" \
  -H "Content-Type: application/json" \
  -d '{"filename": "lecture.mp4"}')

VIDEO_ID=$(echo "$UPLOAD" | jq -r '.videoId')
UPLOAD_URL=$(echo "$UPLOAD" | jq -r '.upload.url')

ARGS=()
for key in $(echo "$UPLOAD" | jq -r '.upload.fields | keys[]'); do
  value=$(echo "$UPLOAD" | jq -r --arg k "$key" '.upload.fields[$k]')
  ARGS+=(-F "$key=$value")
done

# S3's policy requires a Content-Type field starting with video/; it is NOT included
# in fields, add it yourself, before the file field.
curl -X POST "$UPLOAD_URL" "${ARGS[@]}" -F "Content-Type=video/mp4" -F "file=@lecture.mp4"

A successful upload gets a 204 No Content back from S3, no body.

3. Tell vdoX the upload is done

POST /api/v1/videos/:id/complete checks that the file actually landed in storage and queues transcoding. Call this only after step 2 finishes.

shellRequest
curl -X POST https://api.vdox.example.com/api/v1/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/complete \
  -H "Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e"

Response, 200 OK:

jsonResponse
{
  "ok": true
}

4. Poll until it's ready

GET /api/v1/videos/:id returns the video's current state. Poll it (every few seconds is plenty) until status is "ready".

shellRequest
curl https://api.vdox.example.com/api/v1/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV \
  -H "Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e"

While it's transcoding:

jsonResponse, processing
{
  "video": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "tenantId": "01H8Y3ZK5XG5W3E5V8N7Q2R4T6",
    "title": "lecture.mp4",
    "displayName": "lecture.mp4",
    "status": "processing",
    "stage": "transcoding",
    "progress": 42,
    "orientation": null,
    "protection": "aes_128",
    "watermark": "none",
    "allowOffline": false,
    "captionsLangs": [],
    "sourceBytes": 104857600,
    "outputPrefix": "01H8Y3ZK5XG5W3E5V8N7Q2R4T6/01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "outputBytes": null,
    "error": null,
    "createdAt": "2026-07-13T09:00:00.000Z",
    "updatedAt": "2026-07-13T09:02:15.000Z",
    "readyAt": null,
    "download": null,
    "playback": null
  }
}

Once it's ready, playback and download appear:

jsonResponse, ready
{
  "video": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "tenantId": "01H8Y3ZK5XG5W3E5V8N7Q2R4T6",
    "title": "lecture.mp4",
    "displayName": "lecture.mp4",
    "status": "ready",
    "stage": null,
    "progress": 100,
    "orientation": "landscape",
    "protection": "aes_128",
    "watermark": "none",
    "allowOffline": false,
    "captionsLangs": ["en"],
    "sourceBytes": 104857600,
    "outputPrefix": "01H8Y3ZK5XG5W3E5V8N7Q2R4T6/01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "outputBytes": 52428800,
    "error": null,
    "createdAt": "2026-07-13T09:00:00.000Z",
    "updatedAt": "2026-07-13T09:06:40.000Z",
    "readyAt": "2026-07-13T09:06:40.000Z",
    "download": "/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/download",
    "playback": {
      "hlsAes": "/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/hls/master.m3u8",
      "poster": "https://cdn.vdox.example.com/01ARZ3NDEKTSV4RRFFQ69G5FAV/poster.jpg",
      "thumbnailsVtt": "https://cdn.vdox.example.com/01ARZ3NDEKTSV4RRFFQ69G5FAV/thumbnails.vtt",
      "manifestUrl": "https://api.vdox.example.com/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/hls/master.m3u8"
    }
  }
}

See Embed for what to do with playback.hlsAes once the video is ready.

Supported formats and limits

  • Supported source extensions: mp4, mov, mkv, webm, m4v (case insensitive).
  • Filenames must be 1 to 200 characters.
  • Free plan: 10 videos and 5 GB of source storage.

Quota error

If your workspace is already at the free plan's video limit, POST /api/v1/uploads returns 403 instead of creating the video:

jsonResponse, 403
{
  "error": {
    "code": "quota_videos",
    "message": "Plan video limit reached. Upgrade your plan."
  }
}

Going over the storage limit instead returns the same shape with code: "quota_storage". See Authentication for the full error code table.