API reference

All six endpoints in the public API, with exact request and response shapes.

Base URL is your vdoX API host, for example https://api.vdox.example.com. Every endpoint requires Authorization: Bearer vdx_... and every authenticated response carries an X-RateLimit-Remaining header. Any endpoint can return unauthorized (401) or rate_limited (429); those aren't repeated below. See Authentication for the full error table.

Create an upload

POST /api/v1/uploads. Body: filename (string, required, 1 to 200 characters, extension must be mp4, mov, mkv, webm, or m4v). Creates the video row and a presigned S3 POST for the source file.

shellRequest
POST /api/v1/uploads
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Content-Type: application/json

{
  "filename": "lecture.mp4"
}
Response
201 Created

{
  "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 generated per request; use whatever keys and values the response actually returns. See Upload API for the full upload flow.

Errors: invalid_request, invalid_file, workspace_suspended, quota_upload_size, quota_videos, quota_storage. See Authentication for what each code means and its HTTP status.

List videos

GET /api/v1/videos. Query params, all optional: status (one of uploading, uploaded, processing, ready, failed, cancelled), page (positive integer), limit (positive integer).

shellRequest
GET /api/v1/videos?status=ready&page=1&limit=20
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Response
200 OK

{
  "items": [
    {
      "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
      "title": "lecture.mp4",
      "status": "ready",
      "stage": null,
      "progress": 100,
      "durationSeconds": 754,
      "createdAt": "2026-07-13T09:00:00.000Z"
    }
  ],
  "total": 1
}

Response fields

  • stage is the current processing stage (for example "transcoding"), or null when not processing.
  • durationSeconds is null until it's known.
  • total is the total number of videos matching the filter, not just the ones on this page.

Errors: invalid_request. See Authentication for what each code means and its HTTP status.

Get a video

GET /api/v1/videos/:id. Full video state, including playback and download links once it's ready.

shellRequest
GET /api/v1/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Response
200 OK

{
  "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"
    }
  }
}

Response fields

  • download and playback.hlsAes are relative paths against your vdoX playback host, not full URLs.
  • playback.manifestUrl is the same manifest, resolved to an absolute URL, but without a playback token: it isn't directly playable. Mint one via POST /videos/:id/playback-token (below), whose response already returns a ready-to-play, tokenized manifestUrl.
  • playback.poster and playback.thumbnailsVtt are already absolute CDN URLs, usable as-is.
  • download and playback are null until status is "ready".

Errors: not_found. See Authentication for what each code means and its HTTP status.

Complete an upload

POST /api/v1/videos/:id/complete. Verifies the source file landed in storage and queues transcoding. Call this once, right after the presigned upload finishes.

shellRequest
POST /api/v1/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/complete
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Response
200 OK

{
  "ok": true
}

Errors: not_found, unsupported_file, upstream_error. See Authentication for what each code means and its HTTP status.

Mint a playback token

POST /api/v1/videos/:id/playback-token. No request body. Mints a short-lived token for one viewer to watch one video. Call this from your backend, not the browser, see Embed.

shellRequest
POST /api/v1/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/playback-token
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Content-Type: application/json

{
  "viewer": { "label": "student@example.com" }
}
Response
200 OK

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2aWRlb0lkIjoiMDFBUlozTkRFS1RTVjRSUkZGUTY5RzVGQVYi...",
  "expiresAt": "2026-07-13T09:05:00.000Z",
  "manifestUrl": "https://api.vdox.example.com/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/hls/master.m3u8?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "sessionId": "01JAVX7C2H8Q9M4N6P0R3T5W7Y"
}

expiresAt is an ISO timestamp, 5 minutes (300 seconds) after minting by default. manifestUrl is an absolute HLS master URL with this token already appended (?token=...): hand it directly to hls.js or a <video> tag, no further assembly needed.

The request body is optional. Pass { "viewer": { "label": "..." } } (up to 80 characters) to identify the viewer: the label is stamped into the moving watermark and recorded on the playback session for the workspace admin. The response sessionId is the id of the playback session this token opened. Pass it as &session=<sessionId> on the embed URL so the player can heartbeat: when the workspace exceeds its concurrent-stream limit, or a session is revoked, that heartbeat stops playback. Per-plan stream limits are enforced server-side.

Without viewer.label, every caller collapses to a single "anon" identity and counts as ONE viewer against the plan's concurrent-stream limit, no matter how many different end-users are actually watching. Pass a distinct viewer.label per end-user (their id or email works well) if you rely on per-viewer stream limits.

Errors: not_found, not_ready, workspace_suspended, quota_plays, upstream_error. See Authentication for what each code means and its HTTP status.

Report a security event

POST /api/v1/security-events. Records a client-observed capture attempt (screen recording or screenshot) that the server cannot see on its own. Call it from your native app's backend, or the app itself if it holds a key, when iOS UIScreen.isCaptured turns true, an OS screenshot notification fires, or an Android FLAG_SECURE bypass is detected. See Mobile security.

shellRequest
POST /api/v1/security-events
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Content-Type: application/json

{
  "type": "capture_reported",
  "videoId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "sessionId": "01JAVX7C2H8Q9M4N6P0R3T5W7Y",
  "detail": { "signal": "UIScreen.isCaptured" }
}
Response
201 Created

{
  "ok": true
}

type is capture_reported or screenshot_reported. videoId, sessionId, and detail (a small JSON object, up to 2 KB serialized) are optional. The event is always recorded against your own workspace and surfaces on the admin security page.

Errors: invalid_request. See Authentication for what each code means and its HTTP status.

Cancel a video

POST /api/v1/videos/:id/cancel. Stops a video that's still uploading, uploaded, or processing. Use this to free an abandoned upload that would otherwise count against your video quota forever, with no other way to clear it.

shellRequest
POST /api/v1/videos/01ARZ3NDEKTSV4RRFFQ69G5FAV/cancel
Authorization: Bearer vdx_4f2b9e1a7c6d3f805e2b91ac4d7e0f3b1a2c9d8e
Response
200 OK

{
  "ok": true
}

Cancelling a video that's already ready, failed, or cancelled returns 409 conflict instead of an ok response.

Errors: not_found, conflict, upstream_error. See Authentication for what each code means and its HTTP status.