Wolvy DOCS

Reference · v1

API reference

Every endpoint of the Wolvy Developer API, with parameters, example requests in four languages, responses and the errors each one can return.

Base URL https://api.wolvy.net 44 endpoints Postman collection ↓
On this page

Basics#

Base URLhttps://api.wolvy.net — every route is under /v1 except /health
FormatJSON request bodies (Content-Type: application/json) and JSON responses, with real HTTP status codes. 204 responses have no body.
TimestampsRFC 3339 in UTC, e.g. 2026-09-15T09:30:00+00:00. Send date filters in UTC too — an offset is dropped, not converted.
Sizes & timesBytes (_bytes fields) and seconds (duration, watch_time, chapter and moment times).
IDsVideos: hex strings (the same id as in embed URLs). Folders, webhook endpoints, deliveries and security events: integers. Chapters and moments: a positional index.
Unknown fieldsRejected with 400 unknown_parameter, never silently ignored.
BrowsersNot supported. No CORS headers are sent — an API key must never be in front-end code.
Request idsEvery response has X-Request-Id. Log it; support can find any request by it.

Authentication & scopes#

Send your key as a bearer token on every request except /health:

Header
Authorization: Bearer wv_live_8kQ2…x7Pa

Create and revoke keys in Settings → API. A key is wv_live_ plus 32 characters, shown once. Each key carries scopes, can be limited to exact IP addresses (no CIDR ranges), and can have an expiry date.

ScopeGrantsDefault
account:readAccount, player settings.Yes
videos:readList and read videos, folders, captions, chapters, moments; embed URLs.Yes
videos:writeCreate, update and delete all of the above.No
playback:signMint viewer tokens.Yes
analytics:readAnalytics endpoints.Yes
usage:readUsage, usage history, plan.Yes
sessions:readViewer sessions — these include viewer IP addresses.No
security:readSecurity events.No
webhooks:manageWebhook endpoints and deliveries.No

A key created without choosing gets the defaults: read-only plus playback:sign — enough to build an integration, nothing that can delete a library. Missing scope → 403 insufficient_scope; bad key → 401 invalid_api_key.

Errors#

Errors use real status codes and one envelope. Branch on code — it is stable. message is for humans and may change; param names the offending field when there is one.

400 · error body
{
  "error": {
    "type": "invalid_request_error",
    "code": "unknown_parameter",
    "message": "Unknown parameter 'encryption'. Allowed: source_url, title, description, folder_id.",
    "param": "encryption",
    "request_id": "req_5d1e9c2a7b3f40e8a6c1d2f3"
  }
}
StatusCodeMeaningWhat to do
400 invalid_json The body is not valid JSON. Send a JSON object.
400 unsupported_content_type A body was sent with a Content-Type other than JSON. Set Content-Type: application/json.
400 unknown_parameter A body field — or a query parameter on a list or date-range endpoint — this endpoint does not accept. param names it. Remove it. encryption, resolutions and keep_original are decided by your plan and settings.
400 missing_parameter A required field is absent. param names it. Add the field.
400 invalid_parameter A value has the wrong type, format or range — for example limit outside 1–100, from after to, or end not after start. Read message; it says exactly what is wrong.
400 no_fields A PATCH with nothing to change. Send at least one field.
400 invalid_cursor starting_after is not a cursor this API issued. Pass next_cursor back unchanged.
400 invalid_idempotency_key Idempotency-Key is longer than 255 characters. Use a UUID.
400 / 404 folder_not_found 400 when a folder_id you sent is not yours; 404 on /v1/folders/{id}. Check the id with List folders.
401 missing_api_key No Authorization: Bearer … header reached the API. Send the header. Behind a proxy, make sure it forwards Authorization.
401 invalid_api_key Unknown, malformed, revoked or deactivated key. One message for all of them, on purpose. Copy the key again, or create a new one.
401 expired_api_key The key passed its expiry date. Create a new key.
403 insufficient_scope The key lacks the scope this route needs; message names it. Create a key with that scope.
403 ip_not_allowed The key is restricted to IP addresses and this request came from another. Exact match — no CIDR ranges. Add the server’s IP to the key.
403 account_inactive The account is not active. Contact support.
404 unknown_endpoint No route matches the path. Check the /v1 prefix and spelling.
404 not_found A numeric path id (folder, endpoint, chapter or moment index) is not a number. Use the integer id.
404 video_not_found The video does not exist — or belongs to another account. The two are indistinguishable by design. Check the id and the key’s account.
404 caption_not_found No caption for that language. List captions first.
404 chapter_not_found No chapter at that index. Re-read the list — indexes shift after edits.
404 moment_not_found No moment at that index. Re-read the list — indexes shift after edits.
404 session_not_found No such session on your account. Check the session id.
404 webhook_endpoint_not_found No such webhook endpoint on your account. List endpoints.
405 method_not_allowed The path exists but not with this method. The Allow header lists the ones that work. Use a listed method.
409 idempotency_key_reused The same Idempotency-Key was used with a different method, path or body. Use a new key per operation; reuse it only for identical retries.
409 request_in_progress The original request with this key is still running. Wait a moment and retry with the same key.
409 chapter_overlap The range overlaps an existing chapter, which the message names. Adjust start/end.
409 moment_exists A moment already exists at that timestamp. Pick another second or update the existing moment.
422 no_active_plan The account has no active plan. Assign a plan in the dashboard (Billing).
422 insufficient_storage The file (per its Content-Length) is larger than your available storage. Free space or upgrade.
422 invalid_source_url Malformed, not https, or a port other than 443/8443 — at the URL or a redirect hop. Use a plain https URL.
422 source_not_allowed The host resolves to a private or reserved address. Serve the file from a public host.
422 source_unreachable DNS failed, the connection failed, or the server answered non-2xx (a HEAD for videos). Check the URL answers HEAD publicly. GET-only presigned URLs fail here.
422 unsupported_source_type The server sent a Content-Type that is not video. Serve video/* or application/octet-stream.
422 source_too_large Over 20 GiB for a video, or 5 MB for a caption. Compress or split the file.
422 too_many_redirects More than 5 redirects. Link to the final URL.
422 invalid_caption_file The caption does not start with WEBVTT. Convert SRT to WebVTT first.
429 rate_limit_exceeded Per-minute or per-day limit reached for this key. Wait Retry-After seconds.
500 internal_error Something failed on our side. Retry with backoff; quote request_id if it persists.
503 service_unavailable The database or a media server could not be reached. Retry shortly — nothing was changed.

type groups codes broadly: invalid_request_error (400, 404, 405, 409, 422), authentication_error (401), permission_error (403), rate_limit_error (429), api_error (5xx). A resource that belongs to another account answers 404, exactly like one that does not exist.

Pagination#

Lists return {"object": "list", "data": [...], "has_more": true, "next_cursor": "…"}. Pass next_cursor back as starting_after for the next page; limit is 1–100 (default 50). Cursors only go forward and are opaque — don’t build or edit them. A few short lists (captions, chapters, moments, daily and country analytics, usage history, webhook endpoints) return everything at once with has_more: false.

// Walk every video in the library, 100 at a time.
async function* allVideos() {
  let cursor = null;
  do {
    const url = new URL("https://api.wolvy.net/v1/videos");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("starting_after", cursor);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.WOLVY_API_KEY}` } });
    const page = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${page.error.code}`);

    yield* page.data;
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
}

for await (const video of allVideos()) console.log(video.id, video.status);

Idempotency#

Any POST accepts an Idempotency-Key header (up to 255 characters — use a UUID). If a request times out and you retry it with the same key and the same body, you get the original result instead of doing the work twice.

You send the same key…You get
after the first request succeededThe stored response, with header Idempotent-Replay: true
while the first request is still running409 request_in_progress — wait and retry
with a different method, path or body409 idempotency_key_reused
after the first request failed with an errorA fresh attempt — failures are not stored
more than 24 hours laterA fresh attempt — keys expire

Keys are scoped to the API key that used them. Don’t use one when minting playback tokens — a replay would return a stale token.

Rate limits#

Each API key gets 120 requests per minute and 10,000 per day by default. The minute window is the calendar minute; the daily count resets at the start of each day. Requests refused with 401 or 403 do not count.

Response headerMeaning
X-RateLimit-Limit / X-RateLimit-RemainingPer-minute allowance and what is left
X-RateLimit-ResetUnix time the minute window resets
X-RateLimit-Daily-Limit / X-RateLimit-Daily-RemainingThe daily allowance
Retry-AfterOn 429 only: seconds to wait

Need more? Ask support to raise a key’s limits. A client that handles 429 and 5xx gracefully:

import { randomUUID } from "node:crypto";

// Retries 429 and 5xx with backoff. One Idempotency-Key per logical operation,
// reused on every retry, so a POST can never run twice.
export async function wolvy(method, path, body) {
  const key = method === "POST" ? randomUUID() : undefined;
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://api.wolvy.net${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${process.env.WOLVY_API_KEY}`,
        ...(body ? { "Content-Type": "application/json" } : {}),
        ...(key ? { "Idempotency-Key": key } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    if (res.status === 204) return null;
    const data = await res.json();
    if (res.ok) return data;

    const retryable = res.status === 429 || res.status >= 500 || data.error?.code === "request_in_progress";
    if (!retryable || attempt === 5) {
      const err = new Error(`${res.status} ${data.error.code}: ${data.error.message}`);
      err.code = data.error.code;
      err.requestId = data.error.request_id;
      throw err;
    }
    const waitSeconds = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
  }
}

Request builder#

Choose an endpoint, fill in its parameters and copy a working request. Nothing is sent from this page.

REQUEST_BUILDER.EXE

POST/v1/videosscope videos:write

Only the fields listed in the reference are accepted.
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",
    "description": "Week one."
  }'

Nothing is sent from this page. Run the code on your server — the API sends no CORS headers, so browsers cannot call it, by design.

All endpoints#

Resource

Health#

An unauthenticated liveness probe. It deliberately touches the database, so it goes red through the failure that matters rather than only proving PHP runs.

Health check#

GET /health

No authentication Returns 200

No API key. Note the path has no /v1 prefix.

  • When the database is unreachable the answer is 503 — either {"object":"health","status":"degraded","database":"unreachable"} or a service_unavailable error, depending on where the connection failed. Alert on the status code, not the body.

Request

curl "https://api.wolvy.net/health"

Response · 200

200 · application/json
{
  "object": "health",
  "status": "ok",
  "database": "ok"
}
Errors you may see: 503service_unavailable

Resource

Account#

Who the key belongs to, the plan behind it, and the account-wide player settings every embed inherits.

Get account#

GET /v1/account

Scope account:read Returns 200

The natural first call: it proves the key works and tells you its scopes and limits. id is your account id — base64-encode it for embed URLs.

  • plan is null when the account has no active plan — uploads fail with 422 no_active_plan until one is assigned.

Request

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

Response · 200

200 · application/json
{
  "object": "account",
  "id": 42,
  "name": "Acme Academy",
  "username": "acme",
  "email": "[email protected]",
  "status": "active",
  "created_at": "2026-03-02T10:15:00+00:00",
  "plan": {
    "name": "DRM Pro",
    "type": "fixed",
    "protection": "drm",
    "end_date": "2027-03-02T00:00:00+00:00"
  },
  "api_key": {
    "id": 17,
    "name": "Production backend",
    "prefix": "wv_live_8kQ2",
    "last_four": "x7Pa",
    "scopes": [
      "account:read",
      "videos:read",
      "videos:write",
      "playback:sign"
    ],
    "created_at": "2026-09-01T08:00:00+00:00"
  },
  "rate_limits": {
    "per_minute": 120,
    "per_day": 10000,
    "requests_today": 318
  }
}

Get player settings#

GET /v1/settings/player

Scope account:read Returns 200

Read-only view of theme, colours, resolutions, domain rules, subtitle style, watermark and App-Only mode. Change them in the dashboard — they apply to every embed on the account.

  • Check watermark.required: when it is true and the watermark is enabled, playback is refused without a valid viewer token, so every embed must carry ?vt=. See Sign your viewers.
  • An empty allowed_domains means embeds work on any site that sends a Referer. See Domain rules.

Request

curl "https://api.wolvy.net/v1/settings/player" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "player_settings",
  "theme": "1",
  "colors": {
    "player": "#7e3dd9",
    "accent": "#7e3dd9",
    "background": "#f7ee7f"
  },
  "available_resolutions": [
    "240",
    "360",
    "480",
    "720"
  ],
  "allowed_domains": [
    "academy.example.com",
    "*.example.com"
  ],
  "blocked_domains": [],
  "subtitles": {
    "font": "sans",
    "color": "#ffffff",
    "background": "transparent",
    "size": "medium"
  },
  "watermark": {
    "enabled": true,
    "mode": "moving",
    "position": "bottom-right",
    "opacity": 40,
    "required": true
  },
  "app_only_l3": false,
  "keep_original": true
}
Errors you may see: 403insufficient_scope

Resource

Videos#

Your library. Videos are created by URL ingest — Wolvy fetches the file — and move through queued → processing → playable → ready (or failed). Encryption, the resolution ladder and original-file retention come from the plan and account settings, so they are not request fields.

The video object — fields
FieldTypeDescription
idstringThe video id. Also the first path segment of the embed URL.
statusstringqueued · processing · playable (first resolution done, watchable) · ready · failed.
protectionstringdrm or clearkey. Read-only; follows the account’s current plan.
durationnumber | nullSeconds. null until the video is ready.
size_bytesinteger0 until the video is ready.
folder_idinteger | nullnull means the root of the library.
embed_urlstringUnsigned embed URL, present from the moment the video exists. Add ?vt= for signed playback.
direct_play_urlstring | nullThe hosted play page; null when Direct Play is off for the account.
hls_urlstring | nullThe encrypted HLS manifest. It plays through the Wolvy player, not a generic HLS player.
poster_url · animated_poster_urlstring | nullPoster images, generated during encoding.
original_urlstring | nullFor API uploads, the source_url you supplied.
captions · chapters · momentsarraySame shapes as their own endpoints return.
created_at · updated_attimestampRFC 3339, UTC.

List videos#

GET /v1/videos

Scope videos:read Returns 200

Newest first, cursor-paginated. Omitting folder_id returns the whole library, including everything filed in folders.

Query parameters

NameTypeDescription
limitinteger

Page size, 1100. Default 50.

starting_afterstring

The next_cursor from the previous page. Omit for the first page.

folder_idstring

A folder id, or the literal root for unfiled videos.

statusenum

Only videos in this status.

qstring

Substring match on title and description.

created_aftertimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC — an offset is dropped, not converted.

created_beforetimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC.

Request

curl "https://api.wolvy.net/v1/videos?limit=20&status=ready" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "id": "a1b2c3d4e5f60718293a",
      "object": "video",
      "title": "Onboarding",
      "description": "Week one.",
      "status": "ready",
      "protection": "drm",
      "duration": 724.5,
      "size_bytes": 1288490188,
      "folder_id": 7,
      "embed_url": "https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=",
      "direct_play_url": null,
      "hls_url": "https://…/master.m3u8",
      "poster_url": "https://…/poster.jpg",
      "animated_poster_url": null,
      "original_url": "https://media.example.com/uploads/onboarding.mp4",
      "captions": [
        {
          "label": "English",
          "language_code": "en"
        }
      ],
      "chapters": [
        {
          "title": "Introduction",
          "start": 0,
          "end": 45
        }
      ],
      "moments": [
        {
          "label": "Live demo",
          "timestamp": 120
        }
      ],
      "created_at": "2026-09-15T09:30:00+00:00",
      "updated_at": "2026-09-15T09:52:41+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJkIjoiMjAyNi0wOS0xMiAxNjowMjoxMSIsImkiOjkxMn0"
}

Create a video (URL ingest)#

POST /v1/videos

Scope videos:write Returns 202 Accepted Supports Idempotency-Key

Wolvy validates the URL, answers 202 immediately with status: "queued", then downloads and encodes in the background. Poll the status endpoint or wait for the video.ready webhook.

Body (JSON)

NameTypeDescription
source_urlREQUIREDstring

Public https URL of the file (port 443 or 8443). Must answer HEAD with a 2xx and stay reachable until the download finishes. Max 2048 characters.

titlestring

Up to 255 characters. Defaults to the file name from the URL path.

descriptionstring

Free text.

folder_idinteger

Put the video in this folder.

  • There is no encryption, resolutions or keep_original field — sending one returns 400 unknown_parameter. Multi-DRM vs ClearKey follows your plan.
  • The URL is re-checked before the download and on every redirect hop. Full rules: Upload by URL.

Request

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",
    "description": "Week one."
  }'

Response · 202

202 · application/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"
}

Get a video#

GET /v1/videos/{id}

Scope videos:read Returns 200

The full video object.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "id": "a1b2c3d4e5f60718293a",
  "object": "video",
  "title": "Onboarding",
  "description": "Week one.",
  "status": "ready",
  "protection": "drm",
  "duration": 724.5,
  "size_bytes": 1288490188,
  "folder_id": 7,
  "embed_url": "https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=",
  "direct_play_url": null,
  "hls_url": "https://…/master.m3u8",
  "poster_url": "https://…/poster.jpg",
  "animated_poster_url": null,
  "original_url": "https://media.example.com/uploads/onboarding.mp4",
  "captions": [
    {
      "label": "English",
      "language_code": "en"
    }
  ],
  "chapters": [
    {
      "title": "Introduction",
      "start": 0,
      "end": 45
    }
  ],
  "moments": [
    {
      "label": "Live demo",
      "timestamp": 120
    }
  ],
  "created_at": "2026-09-15T09:30:00+00:00",
  "updated_at": "2026-09-15T09:52:41+00:00"
}
Errors you may see: 404video_not_found

Get video status#

GET /v1/videos/{id}/status

Scope videos:read Returns 200

A minimal payload — this is the endpoint to poll while encoding runs, not the full object.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/status" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "id": "a1b2c3d4e5f60718293a",
  "object": "video_status",
  "status": "processing",
  "duration": null,
  "size_bytes": 0,
  "updated_at": "2026-09-15T09:34:10+00:00"
}
Errors you may see: 404video_not_found

Update a video#

PATCH /v1/videos/{id}

Scope videos:write Returns 200

Change the title, description or folder. Send at least one field.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Body (JSON)

NameTypeDescription
titlestring

1–255 characters.

descriptionstring | null

Free text.

folder_idinteger | null

Move to this folder; null moves it to the root.

Request

curl -X PATCH "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Onboarding (revised)",
    "folder_id": 7
  }'

Response · 200

200 · application/json
{
  "id": "a1b2c3d4e5f60718293a",
  "object": "video",
  "title": "Onboarding (revised)",
  "description": "Week one.",
  "status": "ready",
  "protection": "drm",
  "duration": 724.5,
  "size_bytes": 1288490188,
  "folder_id": 7,
  "embed_url": "https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=",
  "direct_play_url": null,
  "hls_url": "https://…/master.m3u8",
  "poster_url": "https://…/poster.jpg",
  "animated_poster_url": null,
  "original_url": "https://media.example.com/uploads/onboarding.mp4",
  "captions": [
    {
      "label": "English",
      "language_code": "en"
    }
  ],
  "chapters": [
    {
      "title": "Introduction",
      "start": 0,
      "end": 45
    }
  ],
  "moments": [
    {
      "label": "Live demo",
      "timestamp": 120
    }
  ],
  "created_at": "2026-09-15T09:30:00+00:00",
  "updated_at": "2026-09-15T09:52:41+00:00"
}

Delete a video#

DELETE /v1/videos/{id}

Scope videos:write Returns 204 No Content

Irreversible. Removes the media files, encryption keys and analytics. Viewer sessions and security events are kept, so your audit trail survives.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

  • Emits video.deleted. Deletions made in the dashboard do not.
  • 503 means the media server could not be reached and the video is still in your library — retry.

Request

curl -X DELETE "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 204

No body.

Resource

Folders#

Folders are a flat set, not a tree, and cannot be renamed. Move videos between them with Update a video.

List folders#

GET /v1/folders

Scope videos:read Returns 200

Newest first, cursor-paginated.

Query parameters

NameTypeDescription
limitinteger

Page size, 1100. Default 50.

starting_afterstring

The next_cursor from the previous page. Omit for the first page.

Request

curl "https://api.wolvy.net/v1/folders?limit=20" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "id": 7,
      "object": "folder",
      "name": "Onboarding",
      "video_count": 12,
      "created_at": "2026-09-02T00:00:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Create a folder#

POST /v1/folders

Scope videos:write Returns 201 Created Supports Idempotency-Key

Body (JSON)

NameTypeDescription
nameREQUIREDstring

1–255 characters.

Request

curl -X POST "https://api.wolvy.net/v1/folders" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "name": "Onboarding"
  }'

Response · 201

201 · application/json
{
  "id": 7,
  "object": "folder",
  "name": "Onboarding",
  "video_count": 0,
  "created_at": "2026-09-15T00:00:00+00:00"
}

Get a folder#

GET /v1/folders/{id}

Scope videos:read Returns 200

Path parameters

NameTypeDescription
idREQUIREDinteger

Folder id.

Request

curl "https://api.wolvy.net/v1/folders/7" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "id": 7,
  "object": "folder",
  "name": "Onboarding",
  "video_count": 12,
  "created_at": "2026-09-02T00:00:00+00:00"
}
Errors you may see: 400folder_not_found404not_found

Delete a folder#

DELETE /v1/folders/{id}

Scope videos:write Returns 204 No Content

The videos inside are not deleted — they move to the root of the library.

Path parameters

NameTypeDescription
idREQUIREDinteger

Folder id.

Request

curl -X DELETE "https://api.wolvy.net/v1/folders/7" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 204

No body.

Errors you may see: 400folder_not_found

Resource

Captions#

WebVTT subtitle tracks. language_code is the identifier: posting a language that already exists replaces it.

List captions#

GET /v1/videos/{id}/captions

Scope videos:read Returns 200

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/captions" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "object": "caption",
      "language_code": "en",
      "label": "English"
    },
    {
      "object": "caption",
      "language_code": "ar",
      "label": "العربية"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
Errors you may see: 404video_not_found

Add or replace a caption#

POST /v1/videos/{id}/captions

Scope videos:write Returns 201 Created Supports Idempotency-Key

Send exactly one of content (the VTT text) or source_url. Returns the full caption list.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Body (JSON)

NameTypeDescription
language_codeREQUIREDstring

A language tag such as en, ar or pt-BR — matches ^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$.

labelstring

What viewers see in the captions menu. Defaults to the language code.

contentstring

The WebVTT file itself. Must contain WEBVTT in its first 64 bytes. Max 5 MB.

source_urlstring

Public https URL of a .vtt file, fetched through the same guard as video ingest. Max 5 MB.

  • Emits caption.ready once the change is picked up (usually within a minute).

Request

curl -X POST "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/captions" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "language_code": "en",
    "label": "English",
    "content": "WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nHello and welcome.\n"
  }'

Response · 201

201 · application/json
{
  "object": "list",
  "data": [
    {
      "object": "caption",
      "language_code": "en",
      "label": "English"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Delete a caption#

DELETE /v1/videos/{id}/captions/{language}

Scope videos:write Returns 204 No Content

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

languageREQUIREDstring

The language_code to remove.

Request

curl -X DELETE "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/captions/en" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 204

No body.

Resource

Chapters#

Named segments on the progress bar. Times are whole seconds and chapters may not overlap. They are addressed by index — the position in the list, sorted by start — so an edit can renumber them: re-read the list after every change instead of caching indexes.

List chapters#

GET /v1/videos/{id}/chapters

Scope videos:read Returns 200

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "object": "chapter",
      "title": "Introduction",
      "start": 0,
      "end": 45
    },
    {
      "index": 1,
      "object": "chapter",
      "title": "Setting up",
      "start": 45,
      "end": 210
    }
  ],
  "has_more": false,
  "next_cursor": null
}
Errors you may see: 404video_not_found

Add a chapter#

POST /v1/videos/{id}/chapters

Scope videos:write Returns 201 Created Supports Idempotency-Key

Returns the full, re-sorted list.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Body (JSON)

NameTypeDescription
titleREQUIREDstring

1–255 characters.

startREQUIREDinteger

Seconds, 0 or more.

endREQUIREDinteger

Seconds; must be greater than start.

Request

curl -X POST "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "title": "Introduction",
    "start": 0,
    "end": 45
  }'

Response · 201

201 · application/json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "object": "chapter",
      "title": "Introduction",
      "start": 0,
      "end": 45
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Update a chapter#

PATCH /v1/videos/{id}/chapters/{index}

Scope videos:write Returns 200

Any of title, start, end. Returns the full, re-sorted list.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

indexREQUIREDinteger

Position in the current list.

Body (JSON)

NameTypeDescription
titlestring

1–255 characters.

startinteger

Seconds.

endinteger

Seconds; greater than start.

Request

curl -X PATCH "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters/0" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Introduction & setup"
  }'

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "object": "chapter",
      "title": "Introduction & setup",
      "start": 0,
      "end": 45
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Delete a chapter#

DELETE /v1/videos/{id}/chapters/{index}

Scope videos:write Returns 204 No Content

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

indexREQUIREDinteger

Position in the current list.

Request

curl -X DELETE "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters/0" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 204

No body.

Errors you may see: 404chapter_not_found

Resource

Moments#

Single-point markers — "highlights" in the dashboard. Timestamps are unique within a video. Indexed like chapters, sorted by timestamp.

List moments#

GET /v1/videos/{id}/moments

Scope videos:read Returns 200

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/moments" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "object": "moment",
      "label": "Live demo",
      "timestamp": 120
    }
  ],
  "has_more": false,
  "next_cursor": null
}
Errors you may see: 404video_not_found

Add a moment#

POST /v1/videos/{id}/moments

Scope videos:write Returns 201 Created Supports Idempotency-Key

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Body (JSON)

NameTypeDescription
labelREQUIREDstring

1–255 characters.

timestampREQUIREDinteger

Seconds, 0 or more. Unique within the video.

Request

curl -X POST "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/moments" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "label": "Live demo",
    "timestamp": 120
  }'

Response · 201

201 · application/json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "object": "moment",
      "label": "Live demo",
      "timestamp": 120
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Update a moment#

PATCH /v1/videos/{id}/moments/{index}

Scope videos:write Returns 200

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

indexREQUIREDinteger

Position in the current list.

Body (JSON)

NameTypeDescription
labelstring

1–255 characters.

timestampinteger

Seconds.

Request

curl -X PATCH "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/moments/0" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Live demo (updated)"
  }'

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "object": "moment",
      "label": "Live demo (updated)",
      "timestamp": 120
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Delete a moment#

DELETE /v1/videos/{id}/moments/{index}

Scope videos:write Returns 204 No Content

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

indexREQUIREDinteger

Position in the current list.

Request

curl -X DELETE "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/moments/0" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 204

No body.

Errors you may see: 404moment_not_found

Resource

Playback#

Embed URLs and signed viewer tokens. The how and why lives in two guides: Sign your viewers and Embed the player.

Create a playback token#

POST /v1/videos/{id}/playback-tokens

Scope playback:sign Returns 201 Created Supports Idempotency-Key

Mints a signed viewer token (vt) for one viewer and returns ready-to-use embed markup. Call it from your backend when you render a page for a signed-in viewer.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Body (JSON)

NameTypeDescription
viewer_idREQUIREDstring

Your identifier for the person watching, 1–64 characters. It is drawn on the watermark and recorded against the playback session.

ttl_secondsinteger

Token lifetime, 6086400. Default 21600 (6 hours).

  • The token is not bound to this video — its payload is only {vid, exp}, so one token plays every video on the account until it expires. The {id} only shapes the returned URLs.
  • Minting through the API costs a request each time. On busy pages, sign locally with your signing secret instead — the output is byte-identical.
  • deep_link opens the video in the Wolvy Player Android app. See App-Only mode.

Request

curl -X POST "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/playback-tokens" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "viewer_id": "user-8813",
    "ttl_seconds": 7200
  }'

Response · 201

201 · application/json
{
  "object": "playback_token",
  "video_id": "a1b2c3d4e5f60718293a",
  "viewer_id": "user-8813",
  "vt": "eyJ2aWQiOiJ1c2VyLTg4MTMiLCJleHAiOjE3OTAwMDAwMDB9.XBNWyMiuLw0y7WlQkzO5s2N-eI8Ltxhs4N0uvDHM4ZM",
  "expires_at": "2026-09-21T14:13:20+00:00",
  "embed_url": "https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=?vt=eyJ2aWQiOiJ1c2VyLTg4MTMiLCJleHAiOjE3OTAwMDAwMDB9.XBNWyMiuLw0y7WlQkzO5s2N-eI8Ltxhs4N0uvDHM4ZM",
  "deep_link": "wolvy://play/a1b2c3d4e5f60718293a/NDI=/eyJ2aWQiOiJ1c2VyLTg4MTMiLCJleHAiOjE3OTAwMDAwMDB9.XBNWyMiuLw0y7WlQkzO5s2N-eI8Ltxhs4N0uvDHM4ZM",
  "iframe_html": "<iframe src=\"https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=?vt=eyJ2aWQiOiJ1c2VyLTg4MTMiLCJleHAiOjE3OTAwMDAwMDB9.XBNWyMiuLw0y7WlQkzO5s2N-eI8Ltxhs4N0uvDHM4ZM\" width=\"100%\" height=\"100%\" frameborder=\"0\" allow=\"autoplay; fullscreen; encrypted-media\" allowfullscreen></iframe>"
}

Get embed URL#

GET /v1/videos/{id}/embed

Scope videos:read Returns 200

The unsigned embed URL and an iframe snippet.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

  • Pasting embed_url into the address bar shows a 403 page — no Referer is sent. That is the domain check working, not a broken link.
  • For DRM playback add picture-in-picture to allow and referrerpolicy="strict-origin-when-cross-origin". Recommended iframe.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/embed" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "embed",
  "video_id": "a1b2c3d4e5f60718293a",
  "embed_url": "https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=",
  "iframe_html": "<iframe src=\"https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=\" width=\"100%\" height=\"100%\" frameborder=\"0\" allow=\"autoplay; fullscreen; encrypted-media\" allowfullscreen></iframe>",
  "direct_play_url": null,
  "note": "The embed URL enforces the account's domain allowlist by Referer, so it returns 403 when opened outside an approved page."
}
Errors you may see: 404video_not_found

Resource

Analytics#

Views, watch time and delivery totals. watch_time is seconds; every storage and bandwidth value is bytes. Ranges are from/to dates, defaulting to the last 30 days.

Overview#

GET /v1/analytics/overview

Scope analytics:read Returns 200

Query parameters

NameTypeDescription
fromdate

YYYY-MM-DD. Default: 29 days before to.

todate

YYYY-MM-DD. Default: today.

  • storage_bytes is the peak over the range — storage is a level, not a flow. bandwidth_bytes is summed.
  • video_count is the current library size, not limited to the range.

Request

curl "https://api.wolvy.net/v1/analytics/overview?from=2026-09-01&to=2026-09-15" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "analytics_overview",
  "from": "2026-09-01",
  "to": "2026-09-15",
  "views": 18240,
  "watch_time": 2911860,
  "storage_bytes": 96636764160,
  "bandwidth_bytes": 412316860416,
  "video_count": 118
}

Daily#

GET /v1/analytics/daily

Scope analytics:read Returns 200

One row per day, oldest first. Not paginated.

Query parameters

NameTypeDescription
fromdate

YYYY-MM-DD. Default: 29 days before to.

todate

YYYY-MM-DD. Default: today.

Request

curl "https://api.wolvy.net/v1/analytics/daily?from=2026-09-01&to=2026-09-15" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "object": "analytics_day",
      "date": "2026-09-14",
      "views": 1302,
      "watch_time": 204118,
      "storage_bytes": 96636764160,
      "bandwidth_bytes": 29451206400
    },
    {
      "object": "analytics_day",
      "date": "2026-09-15",
      "views": 1177,
      "watch_time": 187304,
      "storage_bytes": 96636764160,
      "bandwidth_bytes": 26843545600
    }
  ],
  "has_more": false,
  "next_cursor": null
}

By country#

GET /v1/analytics/countries

Scope analytics:read Returns 200

Sorted by views, highest first. Not paginated.

Query parameters

NameTypeDescription
fromdate

YYYY-MM-DD. Default: 29 days before to.

todate

YYYY-MM-DD. Default: today.

video_idstring

Scope to one video. Lifetime figures — the date range is ignored.

Request

curl "https://api.wolvy.net/v1/analytics/countries?from=2026-09-01&to=2026-09-15" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "object": "analytics_country",
      "country": "EG",
      "views": 11020,
      "watch_time": 1830441
    },
    {
      "object": "analytics_country",
      "country": "SA",
      "views": 3310,
      "watch_time": 512880
    }
  ],
  "has_more": false,
  "next_cursor": null
}

For one video#

GET /v1/videos/{id}/analytics

Scope analytics:read Returns 200

Lifetime totals for a single video.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

  • views and sessions count different things: views is the aggregate counter, sessions counts tracked playback sessions that actually played. Expect them to differ.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/analytics" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "video_analytics",
  "video_id": "a1b2c3d4e5f60718293a",
  "views": 2310,
  "sessions": 1874,
  "watch_time": 488120,
  "engagement_score": 0.71,
  "countries": {
    "EG": 1402,
    "SA": 511,
    "AE": 397
  }
}
Errors you may see: 404video_not_found

Resource

Usage & plan#

Quota and plan state. Every value is bytes (1 GB = 1,073,741,824) with a _bytes suffix, so nothing needs guessing.

Get usage#

GET /v1/usage

Scope usage:read Returns 200

  • danger_zone turns true when the account is close to its allowance — worth alerting on before uploads start failing.

Request

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

Response · 200

200 · application/json
{
  "object": "usage",
  "storage": {
    "limit_bytes": 536870912000,
    "used_bytes": 96636764160,
    "available_bytes": 440234147840
  },
  "bandwidth": {
    "limit_bytes": 1099511627776,
    "used_bytes": 412316860416,
    "available_bytes": 687194767360
  },
  "danger_zone": false,
  "period": {
    "start": "2026-09-02T00:00:00+00:00",
    "end": "2026-10-02T00:00:00+00:00"
  }
}
Errors you may see: 422no_active_plan

Usage history#

GET /v1/usage/history

Scope usage:read Returns 200

Daily rows from the usage ledger, oldest first. Not paginated.

Query parameters

NameTypeDescription
fromdate

YYYY-MM-DD. Default: 29 days before to.

todate

YYYY-MM-DD. Default: today.

Request

curl "https://api.wolvy.net/v1/usage/history?from=2026-09-01&to=2026-09-15" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "object": "usage_day",
      "date": "2026-09-15",
      "storage_bytes": 96636764160,
      "bandwidth_bytes": 26843545600
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Get plan#

GET /v1/plan

Scope usage:read Returns 200

  • protection is why creating a video has no encryption parameter. Changing it means changing plan, in the dashboard.

Request

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

Response · 200

200 · application/json
{
  "object": "plan",
  "name": "DRM Pro",
  "type": "fixed",
  "protection": "drm",
  "storage_limit_bytes": 536870912000,
  "bandwidth_limit_bytes": 1099511627776,
  "start_date": "2026-09-02T00:00:00+00:00",
  "end_date": "2026-10-02T00:00:00+00:00"
}
Errors you may see: 422no_active_plan

Resource

Viewer sessions#

One row per tracked playback. viewer_id is filled only when the session started with a signed viewer token — and then it is server-verified, never something the viewer could set. Sessions outlive the video they describe.

List sessions#

GET /v1/sessions

Scope sessions:read Returns 200

Newest first. Lists only sessions where something actually played (watch_seconds > 0).

Query parameters

NameTypeDescription
limitinteger

Page size, 1100. Default 50.

starting_afterstring

The next_cursor from the previous page. Omit for the first page.

video_idstring

Only sessions for this video.

viewer_idstring

Only sessions for this verified viewer.

countrystring

Two-letter country code.

created_aftertimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC — an offset is dropped, not converted.

created_beforetimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC.

Request

curl "https://api.wolvy.net/v1/sessions?limit=20&viewer_id=user-8813" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "id": "9f2b6c1d3e4a5b60718293a4b5c6d7e8",
      "object": "viewer_session",
      "video_id": "a1b2c3d4e5f60718293a",
      "viewer_id": "user-8813",
      "watch_seconds": 1312,
      "bytes_streamed": 734003200,
      "country": "EG",
      "ip": "203.0.113.24",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36 Edg/140.0",
      "created_at": "2026-09-15T10:40:02+00:00",
      "updated_at": "2026-09-15T11:02:45+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJkIjoiMjAyNi0wOS0xNSAxMTowMjo0NSIsImkiOjU1MzB9"
}

Get a session#

GET /v1/sessions/{session_id}

Scope sessions:read Returns 200

Returns any session you own, including ones that have not played yet.

Path parameters

NameTypeDescription
session_idREQUIREDstring

The session id.

Request

curl "https://api.wolvy.net/v1/sessions/9f2b6c1d3e4a5b60718293a4b5c6d7e8" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "id": "9f2b6c1d3e4a5b60718293a4b5c6d7e8",
  "object": "viewer_session",
  "video_id": "a1b2c3d4e5f60718293a",
  "viewer_id": "user-8813",
  "watch_seconds": 1312,
  "bytes_streamed": 734003200,
  "country": "EG",
  "ip": "203.0.113.24",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36 Edg/140.0",
  "created_at": "2026-09-15T10:40:02+00:00",
  "updated_at": "2026-09-15T11:02:45+00:00"
}
Errors you may see: 404session_not_found

Sessions for one video#

GET /v1/videos/{id}/sessions

Scope sessions:read Returns 200

Same shape and filters as List sessions, scoped to one video.

Path parameters

NameTypeDescription
idREQUIREDstring

The video id (20 hex characters for API uploads) — the same id that appears in embed URLs.

Query parameters

NameTypeDescription
limitinteger

Page size, 1100. Default 50.

starting_afterstring

The next_cursor from the previous page. Omit for the first page.

viewer_idstring

Only sessions for this verified viewer.

countrystring

Two-letter country code.

created_aftertimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC — an offset is dropped, not converted.

created_beforetimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC.

Request

curl "https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/sessions?limit=20" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [],
  "has_more": false,
  "next_cursor": null
}

Resource

Security events#

The audit trail of suspicious and blocked playback. Every event was already handled — these are records, not open incidents. Severity is the signal to act on.

event_typeConfidenceMeaning
drm_device_revokedHighThe device’s DRM client is on the industry revocation list — usually a modified or cracked player.
drm_platform_unverifiedHighPlayback was refused because the platform could not be verified.
device_compromisedHighA mobile device failed integrity checks — likely rooted or modified.
watermark_tamperMediumSomeone tried to remove or hide the viewer-ID overlay during playback.
viewer_token_requiredMediumEnforcement is on and the embed had no valid viewer token.
invalid_app_signatureMediumA request claimed to come from the Wolvy app but was not signed correctly.
drm_license_deniedMediumA licence request was refused.
drm_key_security_failedLowThe device did not meet the security level for high-quality keys; quality was restricted.
invalid_api_keyMediumA request to this API used an invalid key (source: server).
suspicious_clientHeuristicAn unusual browser signature. Logged, never blocked — expect false positives.
scope_deniedReservedAccepted as a filter; not currently recorded.

List security events#

GET /v1/security-events

Scope security:read Returns 200

Newest first, cursor-paginated.

Query parameters

NameTypeDescription
limitinteger

Page size, 1100. Default 50.

starting_afterstring

The next_cursor from the previous page. Omit for the first page.

event_typeenum

One of the event types.

severityenum

info, warn or critical.

video_idstring

Only events for this video.

viewer_idstring

Only events for this verified viewer.

created_aftertimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC — an offset is dropped, not converted.

created_beforetimestamp

ISO 8601 / RFC 3339, inclusive. Send UTC.

  • details carries upstream context and varies by event type — treat it as informational, not a fixed schema.

Request

curl "https://api.wolvy.net/v1/security-events?limit=20&severity=critical" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "id": 20977,
      "object": "security_event",
      "event_type": "drm_device_revoked",
      "severity": "critical",
      "video_id": "a1b2c3d4e5f60718293a",
      "viewer_id": "user-8813",
      "viewer_verified": true,
      "source": "web",
      "ip": "203.0.113.24",
      "country": "EG",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …",
      "session_id": "9f2b6c1d3e4a5b60718293a4b5c6d7e8",
      "details": null,
      "created_at": "2026-09-15T10:41:13+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJpIjoyMDk3N30"
}

Resource

Webhooks#

Register URLs that Wolvy posts events to. Endpoints created here and in the dashboard are the same records. How to receive them safely: Webhooks guide.

List endpoints#

GET /v1/webhook-endpoints

Scope webhooks:manage Returns 200

Every endpoint on the account. Not paginated; never includes secrets.

Request

curl "https://api.wolvy.net/v1/webhook-endpoints" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "id": 3,
      "object": "webhook_endpoint",
      "url": "https://example.com/hooks/wolvy",
      "events": [
        "video.ready",
        "video.failed"
      ],
      "is_active": true,
      "failure_count": 0,
      "created_at": "2026-09-10T08:12:00+00:00",
      "updated_at": "2026-09-10T08:12:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
Errors you may see: 403insufficient_scope

Create an endpoint#

POST /v1/webhook-endpoints

Scope webhooks:manage Returns 201 Created Supports Idempotency-Key

Returns the signing secret once. Store it before you do anything else.

Body (JSON)

NameTypeDescription
urlREQUIREDstring

Public https URL (port 443 or 8443) that resolves to public addresses. Re-checked before every delivery.

eventsREQUIREDstring[]

At least one of the event types.

Request

curl -X POST "https://api.wolvy.net/v1/webhook-endpoints" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13" \
  -d '{
    "url": "https://example.com/hooks/wolvy",
    "events": [
      "video.ready",
      "video.failed"
    ]
  }'

Response · 201

201 · application/json
{
  "id": 3,
  "object": "webhook_endpoint",
  "url": "https://example.com/hooks/wolvy",
  "events": [
    "video.ready",
    "video.failed"
  ],
  "is_active": true,
  "failure_count": 0,
  "created_at": "2026-09-15T12:00:00+00:00",
  "updated_at": "2026-09-15T12:00:00+00:00",
  "secret": "whsec_56b87e856166601d9414d8e3b96f78fe26cbda605a38676a"
}

Get an endpoint#

GET /v1/webhook-endpoints/{id}

Scope webhooks:manage Returns 200

Path parameters

NameTypeDescription
idREQUIREDinteger

Endpoint id.

Request

curl "https://api.wolvy.net/v1/webhook-endpoints/3" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "id": 3,
  "object": "webhook_endpoint",
  "url": "https://example.com/hooks/wolvy",
  "events": [
    "video.ready",
    "video.failed"
  ],
  "is_active": true,
  "failure_count": 0,
  "created_at": "2026-09-10T08:12:00+00:00",
  "updated_at": "2026-09-10T08:12:00+00:00"
}
Errors you may see: 404webhook_endpoint_not_found

Update an endpoint#

PATCH /v1/webhook-endpoints/{id}

Scope webhooks:manage Returns 200

Any of url, events, is_active.

Path parameters

NameTypeDescription
idREQUIREDinteger

Endpoint id.

Body (JSON)

NameTypeDescription
urlstring

New public https URL.

eventsstring[]

Replaces the whole list.

is_activeboolean

true also resets failure_count and resumes pending deliveries.

Request

curl -X PATCH "https://api.wolvy.net/v1/webhook-endpoints/3" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      "video.ready",
      "video.failed",
      "caption.ready"
    ],
    "is_active": true
  }'

Response · 200

200 · application/json
{
  "id": 3,
  "object": "webhook_endpoint",
  "url": "https://example.com/hooks/wolvy",
  "events": [
    "video.ready",
    "video.failed",
    "caption.ready"
  ],
  "is_active": true,
  "failure_count": 0,
  "created_at": "2026-09-10T08:12:00+00:00",
  "updated_at": "2026-09-15T12:30:00+00:00"
}

Delete an endpoint#

DELETE /v1/webhook-endpoints/{id}

Scope webhooks:manage Returns 204 No Content

Also deletes its delivery history, including anything still waiting to retry.

Path parameters

NameTypeDescription
idREQUIREDinteger

Endpoint id.

Request

curl -X DELETE "https://api.wolvy.net/v1/webhook-endpoints/3" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 204

No body.

Errors you may see: 404webhook_endpoint_not_found

Send a test delivery#

POST /v1/webhook-endpoints/{id}/test

Scope webhooks:manage Returns 202 Accepted Supports Idempotency-Key

Queues a webhook.test event so you can check your signature code without waiting for a video to encode. Delivery is asynchronous — check List deliveries.

Path parameters

NameTypeDescription
idREQUIREDinteger

Endpoint id.

Request

curl -X POST "https://api.wolvy.net/v1/webhook-endpoints/3/test" \
  -H "Authorization: Bearer $WOLVY_API_KEY" \
  -H "Idempotency-Key: 5b6f1c2e-9a0d-4f3e-8c71-2d4b6a9e0f13"

Response · 202

202 · application/json
{
  "object": "webhook_delivery",
  "id": 1841,
  "event_type": "webhook.test",
  "endpoint_id": 3,
  "status": "pending"
}
Errors you may see: 404webhook_endpoint_not_found

List deliveries#

GET /v1/webhook-deliveries

Scope webhooks:manage Returns 200

The debugging view when your endpoint is not receiving what you expect. Newest first.

Query parameters

NameTypeDescription
limitinteger

Page size, 1100. Default 50.

starting_afterstring

The next_cursor from the previous page. Omit for the first page.

endpoint_idinteger

Only deliveries to this endpoint.

statusenum

pending, delivered or failed.

  • Delivered rows are kept 30 days, failed rows 90 days.

Request

curl "https://api.wolvy.net/v1/webhook-deliveries?limit=20&status=failed" \
  -H "Authorization: Bearer $WOLVY_API_KEY"

Response · 200

200 · application/json
{
  "object": "list",
  "data": [
    {
      "id": 1841,
      "object": "webhook_delivery",
      "endpoint_id": 3,
      "event_type": "video.ready",
      "attempt": 5,
      "status": "failed",
      "response_code": 500,
      "next_retry_at": null,
      "created_at": "2026-09-15T09:52:43+00:00",
      "delivered_at": null
    }
  ],
  "has_more": true,
  "next_cursor": "eyJpIjoxODM0fQ"
}