Guide · ingest & encoding
Upload by URL
You give Wolvy a link; Wolvy downloads the file, encodes every resolution, encrypts it for your plan and tells you when it is ready. Here is what the link must satisfy and how to follow the video from queued to ready.
On this page
How ingest works#
- You send a URLWolvy checks it immediately — https, public address, reachable, a video, fits your storage — and answers
202with a video id, or a422that says what is wrong. - Wolvy downloads the fileA background worker fetches it, re-checking the address at download time. A failed download is retried; the third failure marks the video
failed. - Encoding and encryptionThe file becomes a resolution ladder, encrypted with Multi-DRM or ClearKey according to your plan.
video.createdfires when this starts. - Playable, then readyOnce the first resolution exists the status is
playable— viewers can watch. When every resolution is done it isreadyandvideo.readyfires.
Source URL rules#
The rules exist because Wolvy fetches the URL from inside its own network. Each one is checked when you call the API, and again before the download — including on every redirect.
| Rule | Detail | If not |
|---|---|---|
| https only | Port 443 or 8443. | invalid_source_url |
| Public address | The hostname — and every redirect hop — must resolve to public IPs. Private, loopback and reserved ranges are refused. | source_not_allowed |
Answers HEAD | Wolvy probes with a HEAD request that must return 2xx. | source_unreachable |
| Looks like video | If a Content-Type is sent it must be video/*, application/octet-stream, application/mp4 or binary/octet-stream. | unsupported_source_type |
| Size | Up to 20 GiB, and no larger than your available storage (when Content-Length is sent). | source_too_large · insufficient_storage |
| Redirects | At most 5. | too_many_redirects |
| Stays up | The link must keep working until the download finishes — a large file can take a while. Don’t expire it in minutes. | video.failed later |
Create the video#
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.",
"folder_id": 7
}' import { randomUUID } from "node:crypto";
const res = await fetch("https://api.wolvy.net/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WOLVY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(), // reuse the same key when you retry
},
body: JSON.stringify({
source_url: "https://media.example.com/uploads/onboarding.mp4",
title: "Onboarding",
description: "Week one.",
folder_id: 7,
}),
});
const video = await res.json();
if (!res.ok) throw new Error(`${res.status} ${video.error.code}`); import os
import uuid
import requests
res = requests.post(
"https://api.wolvy.net/v1/videos",
headers={
"Authorization": f"Bearer {os.environ['WOLVY_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()), # reuse the same key when you retry
},
json={
"source_url": "https://media.example.com/uploads/onboarding.mp4",
"title": "Onboarding",
"description": "Week one.",
"folder_id": 7,
},
timeout=30,
)
video = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {video['error']['code']}") <?php
$ch = curl_init('https://api.wolvy.net/v1/videos');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WOLVY_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)), // reuse the same key when you retry
],
CURLOPT_POSTFIELDS => json_encode([
'source_url' => 'https://media.example.com/uploads/onboarding.mp4',
'title' => 'Onboarding',
'description' => 'Week one.',
'folder_id' => 7,
]),
]);
$video = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException("$status {$video['error']['code']}");
} | Field | Type | Description |
|---|---|---|
source_urlREQUIRED | string | The https link, up to 2048 characters. |
title | string | Up to 255 characters. Defaults to the file name in the URL. |
description | string | Free text. |
folder_id | integer | An existing folder. Create folders with POST /v1/folders. |
The 202 response is the full video object with status: "queued". embed_url is already usable; duration, size_bytes and poster_url fill in when the video is ready.
The status lifecycle#
Step through it — the simulator shows what GET /v1/videos/{id}/status returns and which webhooks your server receives at each point.
Accepted with 202. Wolvy is downloading your source URL.
GET /v1/videos/{id}/status
{
"id": "a1b2c3d4e5f60718293a",
"object": "video_status",
"status": "queued",
"duration": null,
"size_bytes": 0,
"updated_at": "2026-09-15T09:30:00+00:00"
}What your server hears
- Your
POST /v1/videosreturned202·status: "queued"
| Status | Meaning | Webhook |
|---|---|---|
queued | Accepted; the source is being downloaded. | — |
processing | Handed to encoding. | video.created |
playable | The first resolution is ready. Embeds already play. | — |
ready | All resolutions encoded; duration, size and poster are set. | video.ready |
failed | Download or encoding failed. Failed encodes cannot be retried through the API — create the video again. | video.failed |
Poll or listen#
Poll the status endpoint
GET /v1/videos/{id}/status is deliberately tiny. Poll every 10–30 seconds from a background job and stop at ready or failed. Mind the 120 requests-per-minute key limit if you watch many uploads. Polling code.
Listen for webhooks
Subscribe to video.ready and video.failed. Events are picked up by jobs that run every minute, so expect them within a minute or two. Set up webhooks.
Captions, chapters & moments#
Captions
WebVTT, one track per language_code. Send the file inline as content or as a source_url (max 5 MB, fetched with the same address checks as videos). Posting a language again replaces it. SRT is not accepted — convert it to WebVTT first.
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": "ar",
"label": "العربية",
"source_url": "https://media.example.com/captions/onboarding.ar.vtt"
}' import { randomUUID } from "node:crypto";
const res = await fetch("https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/captions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WOLVY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(), // reuse the same key when you retry
},
body: JSON.stringify({
language_code: "ar",
label: "العربية",
source_url: "https://media.example.com/captions/onboarding.ar.vtt",
}),
});
const captions = await res.json();
if (!res.ok) throw new Error(`${res.status} ${captions.error.code}`); import os
import uuid
import requests
res = requests.post(
"https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/captions",
headers={
"Authorization": f"Bearer {os.environ['WOLVY_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()), # reuse the same key when you retry
},
json={
"language_code": "ar",
"label": "العربية",
"source_url": "https://media.example.com/captions/onboarding.ar.vtt",
},
timeout=30,
)
captions = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {captions['error']['code']}") <?php
$ch = curl_init('https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/captions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WOLVY_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)), // reuse the same key when you retry
],
CURLOPT_POSTFIELDS => json_encode([
'language_code' => 'ar',
'label' => 'العربية',
'source_url' => 'https://media.example.com/captions/onboarding.ar.vtt',
]),
]);
$captions = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException("$status {$captions['error']['code']}");
} Chapters
Named ranges on the progress bar, in whole seconds, never overlapping. The response is the whole list, sorted by start, with each chapter’s index.
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": "Setting up",
"start": 45,
"end": 210
}' import { randomUUID } from "node:crypto";
const res = await fetch("https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WOLVY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(), // reuse the same key when you retry
},
body: JSON.stringify({
title: "Setting up",
start: 45,
end: 210,
}),
});
const chapters = await res.json();
if (!res.ok) throw new Error(`${res.status} ${chapters.error.code}`); import os
import uuid
import requests
res = requests.post(
"https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters",
headers={
"Authorization": f"Bearer {os.environ['WOLVY_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()), # reuse the same key when you retry
},
json={"title": "Setting up", "start": 45, "end": 210},
timeout=30,
)
chapters = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {chapters['error']['code']}") <?php
$ch = curl_init('https://api.wolvy.net/v1/videos/a1b2c3d4e5f60718293a/chapters');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WOLVY_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)), // reuse the same key when you retry
],
CURLOPT_POSTFIELDS => json_encode([
'title' => 'Setting up',
'start' => 45,
'end' => 210,
]),
]);
$chapters = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException("$status {$chapters['error']['code']}");
} Moments
Single labelled points (“highlights” in the dashboard): {"label": "Live demo", "timestamp": 120}. Timestamps are unique per video; a clash is 409 moment_exists. Full reference: Moments.
Errors you’ll meet#
| Code | Status | What to do |
|---|---|---|
no_active_plan | 422 | The account has no active plan. Assign a plan in the dashboard (Billing). |
insufficient_storage | 422 | The file (per its Content-Length) is larger than your available storage. Free space or upgrade. |
invalid_source_url | 422 | Malformed, not https, or a port other than 443/8443 — at the URL or a redirect hop. Use a plain https URL. |
source_not_allowed | 422 | The host resolves to a private or reserved address. Serve the file from a public host. |
source_unreachable | 422 | 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. |
unsupported_source_type | 422 | The server sent a Content-Type that is not video. Serve video/* or application/octet-stream. |
source_too_large | 422 | Over 20 GiB for a video, or 5 MB for a caption. Compress or split the file. |
too_many_redirects | 422 | More than 5 redirects. Link to the final URL. |
folder_not_found | 400 / 404 | 400 when a folder_id you sent is not yours; 404 on /v1/folders/{id}. Check the id with List folders. |
unknown_parameter | 400 | 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. |
A download that fails after the 202 does not produce an HTTP error — you learn about it from the status (failed) or the video.failed webhook, whose data.error explains what happened.
What Wolvy decides for you#
Three things you might expect to send are deliberately not request fields. Sending them returns 400 unknown_parameter rather than being silently ignored:
| You might send | Decided by | Read it from |
|---|---|---|
encryption | Your plan — Multi-DRM or ClearKey. | protection on Get plan |
resolutions | Account setting: Settings → Player → Video quality. | available_resolutions on player settings |
keep_original | Account setting: Settings → Storage. | keep_original on player settings |
Something wrong or unclear on this page? Email [email protected] — include the page name.