Wolvy DOCS

Get started · about 10 minutes

Quickstart

From zero to a protected video playing on your own page: create a key, call the API, upload by URL, wait for encoding, and embed it for a signed-in viewer.

Languages cURL · Node.js · Python · PHP Scopes account:read videos:write playback:sign
On this page

Before you start#

  • A Wolvy account with an active plan. Uploads answer 422 no_active_plan without one.
  • A server to run code on. The API refuses to be called from a browser: it sends no CORS headers, because an API key in a browser is a leaked key.
  • A video file at a public https URL. Wolvy downloads it for you — there is no file-upload endpoint.
  • If your account restricts embeds to certain domains (Settings → Protection → Allowed domains), the site you will embed on must be on that list.

1 · Create an API key#

  1. Open the dashboardGo to Settings → API and create a key. Name it after where it will run — Production backend, Staging.
  2. Pick scopesFor this guide: account:read, videos:write and playback:sign. The default set is read-only plus signing, so tick videos:write yourself.
  3. Copy it onceThe full key (wv_live_ + 32 characters) is shown only at creation. Wolvy stores a hash and cannot show it again.
.env
# .env — never commit this file
WOLVY_API_KEY=wv_live_…

2 · Make your first call#

GET /v1/account is the natural first request: it proves the key works and shows its scopes and rate limits.

curl "https://api.wolvy.net/v1/account" \
  -H "Authorization: Bearer $WOLVY_API_KEY"
200 · response (trimmed)
{
  "object": "account",
  "id": 42,
  "name": "Acme Academy",
  "status": "active",
  "plan": { "name": "DRM Pro", "type": "fixed", "protection": "drm", "end_date": "2027-03-02T00:00:00+00:00" },
  "api_key": { "id": 17, "prefix": "wv_live_8kQ2", "last_four": "x7Pa", "scopes": ["account:read", "videos:read", "videos:write", "playback:sign"] },
  "rate_limits": { "per_minute": 120, "per_day": 10000, "requests_today": 1 }
}

Note id — that is your account id, and it appears (base64-encoded) in every embed URL. A 401 invalid_api_key means the key was mistyped or revoked; 403 insufficient_scope names the scope the key is missing.

3 · Upload a video by URL#

Give Wolvy a public https link to the file. The request returns straight away with 202 Accepted; downloading and encoding happen in the background.

curl -X POST "https://api.wolvy.net/v1/videos" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "source_url": "https://media.example.com/uploads/onboarding.mp4",
    "title": "Onboarding"
  }'
202 · response
{
  "id": "a1b2c3d4e5f60718293a",
  "object": "video",
  "title": "Onboarding",
  "description": "Week one.",
  "status": "queued",
  "protection": "drm",
  "duration": null,
  "size_bytes": 0,
  "folder_id": null,
  "embed_url": "https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=",
  "direct_play_url": null,
  "hls_url": null,
  "poster_url": null,
  "animated_poster_url": null,
  "original_url": "https://media.example.com/uploads/onboarding.mp4",
  "captions": [],
  "chapters": [],
  "moments": [],
  "created_at": "2026-09-15T09:30:00+00:00",
  "updated_at": "2026-09-15T09:30:00+00:00"
}

Save the returned id. There is no encryption or resolutions field — Multi-DRM or ClearKey comes from your plan, and sending either is a 400 unknown_parameter.

4 · Wait until it plays#

The video moves through queued → processing → playable → ready. playable means the first resolution is done and viewers can already watch while the rest encode. Poll the lightweight status endpoint with backoff:

const API = "https://api.wolvy.net/v1";
const headers = { Authorization: `Bearer ${process.env.WOLVY_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Run this in a background job, not inside a web request.
export async function waitUntilPlayable(videoId, { timeoutMs = 60 * 60 * 1000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  let delay = 10_000;
  while (Date.now() < deadline) {
    const res = await fetch(`${API}/videos/${encodeURIComponent(videoId)}/status`, { headers });
    const body = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${body.error.code}`);
    if (body.status === "playable" || body.status === "ready") return body;
    if (body.status === "failed") throw new Error(`Encoding failed for ${videoId}`);
    await sleep(delay);
    delay = Math.min(delay * 1.5, 60_000); // 10 s, 15 s, 22 s … up to 60 s
  }
  throw new Error("Timed out waiting for the video");
}

5 · Put it on a page#

When a signed-in viewer opens the page, ask Wolvy for a playback token for that viewer and render the returned embed_url in an iframe. The token carries the viewer’s id, so the watermark and your session logs name them — and they cannot swap in someone else’s.

// Express. requireLogin is your own auth middleware.
app.get("/lessons/:videoId", requireLogin, async (req, res) => {
  const videoId = encodeURIComponent(req.params.videoId);
  const r = await fetch(`https://api.wolvy.net/v1/videos/${videoId}/playback-tokens`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.WOLVY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ viewer_id: String(req.user.id), ttl_seconds: 7200 }),
  });
  const token = await r.json();
  if (!r.ok) return res.status(502).send("This video is unavailable right now.");

  res.set("Cache-Control", "private, no-store"); // the page carries this viewer's token
  res.send(`<!doctype html>
<title>Lesson</title>
<div style="aspect-ratio: 16 / 9; max-width: 960px">
  <iframe src="${token.embed_url}" title="Lesson video"
    allow="autoplay; fullscreen; encrypted-media; picture-in-picture" allowfullscreen
    referrerpolicy="strict-origin-when-cross-origin"
    style="width: 100%; height: 100%; border: 0"></iframe>
</div>`);
});
  • Every attribute on that iframe matters. encrypted-media lets DRM start inside the frame; the referrerpolicy keeps the domain check working. Why each one is there.
  • Mark the page private. It contains one viewer’s token; a shared cache would hand it to everybody.
  • Busy page? Sign tokens on your own server instead — no request per page view. Sign it yourself.

Next steps#