# Upload by URL

> How Wolvy fetches a video from a public https URL, the rules that URL must pass, the queued → processing → playable → ready lifecycle, and adding captions.

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

_Guide · ingest & encoding_

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

Endpoint `POST /v1/videos` · Scope `videos:write`

## How ingest works

1. **You send a URL**Wolvy checks it immediately — https, public address, reachable, a video, fits your storage — and answers `202` with a video id, or a `422` that says what is wrong.
2. **Wolvy downloads the file**A background worker fetches it, re-checking the address at download time. A failed download is retried; the third failure marks the video `failed`.
3. **Encoding and encryption**The file becomes a resolution ladder, encrypted with Multi-DRM or ClearKey according to your plan. `video.created` fires when this starts.
4. **Playable, then ready**Once the first resolution exists the status is `playable` — viewers can watch. When every resolution is done it is `ready` and `video.ready` fires.

> **Note — No direct file upload in the API**
> Ingest is by URL only. To upload a file from a computer, use the Wolvy dashboard; the video then appears in `GET /v1/videos` like any other.

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

> **Careful — Presigned storage URLs**
> Many presigned links (S3, R2, GCS…) are signed for one HTTP method. A link presigned for `GET` can refuse Wolvy’s `HEAD` probe and fail with `source_unreachable`. Use a link that answers `HEAD` as well, with an expiry of hours rather than minutes.

## Create the video

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

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

| Field | Type | Description |
| --- | --- | --- |
| `source_url` (required) | 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`](https://docs.wolvy.net/api/#create-folder). |

The `202` response is the full [video object](https://docs.wolvy.net/api/#videos) 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.

_[Interactive: Lifecycle Sim — runs on https://docs.wolvy.net/uploads/]_

| 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

_Simple_

### 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](https://docs.wolvy.net/quickstart/#wait).

_Recommended_

### 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](https://docs.wolvy.net/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.

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

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

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

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

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

> **Careful — Indexes shift**
> Chapters and moments are addressed by position. Adding or deleting one renumbers those after it — re-read the list after every change rather than storing indexes.

### 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](https://docs.wolvy.net/api/#moments).

## Errors you’ll meet

| Code | Status | What to do |
| --- | --- | --- |
| [`no_active_plan`](https://docs.wolvy.net/api/#error-no_active_plan) | 422 | The account has no active plan. Assign a plan in the dashboard (Billing). |
| [`insufficient_storage`](https://docs.wolvy.net/api/#error-insufficient_storage) | 422 | The file (per its `Content-Length`) is larger than your available storage. Free space or upgrade. |
| [`invalid_source_url`](https://docs.wolvy.net/api/#error-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`](https://docs.wolvy.net/api/#error-source_not_allowed) | 422 | The host resolves to a private or reserved address. Serve the file from a public host. |
| [`source_unreachable`](https://docs.wolvy.net/api/#error-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`](https://docs.wolvy.net/api/#error-unsupported_source_type) | 422 | The server sent a `Content-Type` that is not video. Serve `video/*` or `application/octet-stream`. |
| [`source_too_large`](https://docs.wolvy.net/api/#error-source_too_large) | 422 | Over 20 GiB for a video, or 5 MB for a caption. Compress or split the file. |
| [`too_many_redirects`](https://docs.wolvy.net/api/#error-too_many_redirects) | 422 | More than 5 redirects. Link to the final URL. |
| [`folder_not_found`](https://docs.wolvy.net/api/#error-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`](https://docs.wolvy.net/api/#error-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](https://docs.wolvy.net/api/#get-plan) |
| `resolutions` | Account setting: Settings → Player → Video quality. | `available_resolutions` on [player settings](https://docs.wolvy.net/api/#get-player-settings) |
| `keep_original` | Account setting: Settings → Storage. | `keep_original` on player settings |

## Common questions

### Can I upload a video file straight to the API?

No. `POST /v1/videos` takes a `source_url` and Wolvy downloads the file itself. To send a file from a computer, upload it in the dashboard — it then appears in `GET /v1/videos` like any other video.

### Why does my presigned S3 link fail with source_unreachable?

Wolvy probes the URL with a `HEAD` request before downloading, and a link presigned for `GET` alone refuses that probe. Presign for `HEAD` as well, and give the link hours rather than minutes — it has to stay alive until the download finishes.

### How do I know when a video is ready?

Poll [`GET /v1/videos/{id}/status`](https://docs.wolvy.net/api/#get-video-status) every 10–30 seconds, or subscribe to the `video.ready` [webhook](https://docs.wolvy.net/webhooks/). A video is already watchable at `playable`, before the last resolution finishes.

### Can I retry a failed encode?

No. A `failed` video cannot be restarted through the API — create the video again with a source URL that works. The `video.failed` webhook carries `data.error`, which says what went wrong.

### How large can a video be?

Up to 20 GiB per file, and never more than the storage left on your plan. When the source sends a `Content-Length`, Wolvy answers [`source_too_large`](https://docs.wolvy.net/api/#error-source_too_large) or [`insufficient_storage`](https://docs.wolvy.net/api/#error-insufficient_storage) before it starts downloading.

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