VidReef

VidReef API

Connect VidReef to your course platform, CMS, or internal tools: list videos and folders, get embed codes and transcripts, import videos from a URL, and get a webhook when a video or transcript is ready. The API is available on Business and Enterprise plans; importing through the API is Enterprise only.

Getting started

The workspace owner creates keys under Settings → API & webhooks. Send the key in the Authorization header of every request. Each key works with one workspace. Keep them on your server; never put a key in a web page or app.

curl https://vidreef.com/api/v1/videos -H "Authorization: Bearer vr_live_…"

All responses are JSON: a single object in data, or a list in data with next_cursor. Times are ISO 8601 in UTC.

Limits and errors

Each key can make 60 requests a minute. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix time). Over the limit you get 429 with Retry-After in seconds. Use webhooks instead of polling where you can.

Errors share one shape, with a stable code and a readable message:

{ "error": { "code": "not_found", "message": "No video with that id in this workspace." } }
  • 401 unauthorized: missing, wrong, or revoked key
  • 403 plan_required: the plan doesn't include the API, or (for imports) isn't Enterprise
  • 403 quota_exceeded: storage or the month's processing is used up
  • 400: a bad parameter (the code says which)
  • 404 not_found, 429 rate_limited, 500 server_error

Endpoints

GET/videos

Videos in the workspace, newest first. Optional query parameters: folder_id, status (uploading, processing, ready, failed), limit (1–100, default 25), and cursor (the next_cursor from the previous page; null on the last page).

curl "https://vidreef.com/api/v1/videos?status=ready&limit=100" -H "Authorization: Bearer vr_live_…"

GET/videos/{id}

One video, with its embed codes and share link. thumbnail_url works for public videos.

{
  "data": {
    "id": "k3J9xQ2mPa",
    "title": "Iron condors, part 2",
    "description": "",
    "status": "ready",
    "error": null,
    "duration_seconds": 1834.2,
    "width": 1920,
    "height": 1080,
    "folder_ids": ["fld_8Hc2kd93JsLq0aZe"],
    "transcript_status": "succeeded",
    "thumbnail_url": "https://vidreef.com/api/thumb/k3J9xQ2mPa.jpg",
    "iframe": "<iframe src=\"https://vidreef.com/embed/k3J9xQ2mPa\" style=\"aspect-ratio:1920/1080;width:100%;border:0\" allow=\"autoplay; fullscreen; picture-in-picture\" allowfullscreen></iframe>",
    "embed_url": "https://vidreef.com/embed/k3J9xQ2mPa",
    "share_url": "https://vidreef.com/v/k3J9xQ2mPa",
    "created_at": "2026-09-29T14:03:11.000Z",
    "updated_at": "2026-09-29T14:09:52.000Z"
  }
}

GET/videos/{id}/transcript

The video's transcript. format is json (default: timed segments), txt (plain paragraphs), txt-timestamps (paragraphs starting with [m:ss]), vtt, or srt. Until it's ready you get 404 transcript_not_ready.

{
  "data": {
    "video_id": "k3J9xQ2mPa",
    "language": "en",
    "segments": [
      { "start_seconds": 0.48, "end_seconds": 4.1, "text": "Welcome back. Today we're adjusting the condor." }
    ]
  }
}

GET/folders

Every folder in the workspace. Folders nest: parent_id is the enclosing folder, or null at the top level.

{ "data": [ { "id": "fld_8Hc2kd93JsLq0aZe", "name": "Options 101", "parent_id": null, "created_at": "…" } ] }

POST/imports

Enterprise. Imports one video from a public http(s) URL to the original file (MP4, MOV, and so on). We copy it, process it, and transcribe it, just like an upload. Body fields: url (required), title (defaults to the file name), description, folder_id, and external_id (your own reference; sending the same one again the same day returns the existing import instead of a duplicate). Returns 202.

curl -X POST https://vidreef.com/api/v1/imports \
  -H "Authorization: Bearer vr_live_…" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://files.example.com/lesson-12.mp4", "title": "Lesson 12", "external_id": "lesson-12"}'
{
  "data": {
    "id": "imi_Qm2Jd8sLk3HvA0pZ",
    "status": "pending",
    "url": "https://files.example.com/lesson-12.mp4",
    "external_id": "lesson-12",
    "video_id": null,
    "video_status": null,
    "error": null,
    "created_at": "…"
  }
}

GET/imports/{id}

Enterprise. An import's progress. status is pending, running, imported, or failed (with error). Once the file is copied, video_id is set and video_status follows processing; the video.ready webhook tells you when it can play.

Webhooks

Add a URL under Settings → API & webhooks and choose events. We POST JSON to it:

  • video.ready: a video finished processing and can play. data.video is the same object as GET /videos/{id}.
  • transcript.ready: a transcript was created or replaced (including edits). data has video_id, language, and source; fetch the text from the transcript endpoint.
{ "id": "evt_…", "type": "video.ready", "created_at": "2026-09-29T14:09:52.000Z", "data": { "video": { … } } }

Answer with any 2xx within 10 seconds. Otherwise we retry after 1 and 5 minutes, then 30 minutes, 2, 6, 12, and 24 hours (about three days in all). A URL that fails for three days is turned off; turn it back on in settings. The same event can arrive more than once, so use its id to skip duplicates.

Verifying the signature

Each request has a VidReef-Signature header: t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256 of t, a period, and the raw body, keyed with the endpoint's signing secret (shown once when you add it). Reject requests that don't match or are more than 5 minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a string, before JSON.parse)
function verifyVidReef(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

What the API doesn't do

The API doesn't hand out original video files or streaming files; videos play through VidReef embeds and share links, which keep your domain restrictions and analytics working. To take a full copy of your library, originals included, use Export your library in settings.

Questions or need something the API doesn't cover? Email support@vidreef.com.