# Quickstart

> Create an API key, make your first call to the Wolvy API, upload a video by URL, wait for it to encode and put it on a page with a signed viewer token.

Source: https://docs.wolvy.net/quickstart/ · Wolvy Developer API v1 · updated 2026-11-12

_Get started · about 10 minutes_

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`

## 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 dashboard**Go to Settings → API and create a key. Name it after where it will run — _Production backend_, _Staging_.
2. **Pick scopes**For 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 once**The full key (`wv_live_` + 32 characters) is shown only at creation. Wolvy stores a hash and cannot show it again.

_.env_

```bash
# .env — never commit this file
WOLVY_API_KEY=wv_live_…
```

> **Security — Keep keys server-side**
> Anyone holding a key acts as your account within its scopes. Keep it in an environment variable or secret manager, give each environment its own key, and restrict production keys to your servers’ IP addresses when you create them.

## 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.

```bash
curl "https://api.wolvy.net/v1/account" \
  -H "Authorization: Bearer $WOLVY_API_KEY"
```

_Also on the page in Node.js, Python, PHP._

_200 · response (trimmed)_

```json
{
  "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.

```bash
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"
  }'
```

_Also on the page in Node.js, Python, PHP._

_202 · response_

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

> **Tip — The Idempotency-Key makes retries safe**
> If the connection drops before you see the response, send the _same_ request with the _same_ key: you get the original response back instead of a second video. [How idempotency works](https://docs.wolvy.net/api/#idempotency).

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:

```js
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");
}
```

_Also on the page in Python, PHP._

> **Note — In production, listen instead of polling**
> Subscribe to `video.ready` and `video.failed` and Wolvy tells you when it is done. See [Webhooks](https://docs.wolvy.net/webhooks/) and the [lifecycle simulator](https://docs.wolvy.net/uploads/#lifecycle).

## 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.

```js
// 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>`);
});
```

_Also on the page in Python, PHP._

- **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](https://docs.wolvy.net/embedding/#iframe).
- **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](https://docs.wolvy.net/viewer-tokens/#sign-locally).

## Next steps

_Needs your code_

### Sign your viewers

The token format, six languages, test vectors and an interactive lab.

_Needs your code_

### Webhooks

Receive `video.ready` and verify every delivery.

_Guide_

### Embed the player

Domain rules, responsive sizing, App-Only mode.

_Reference_

### API reference

Every endpoint, with a request builder.

Something wrong or unclear on this page? Email [contact@wolvy.net](mailto:contact@wolvy.net?subject=Docs%3A%20Quickstart) — include the page name.
