API Reference

Webhooks

Get notified the moment a video finishes instead of polling. Pass webhookUrl when you create a video and Vsub POSTs a JSON payload to that URL once the video reaches a terminal status.

When events fire

Exactly one POST is sent per video, the first time it reaches one of these terminal statuses:

  • video.completed — generation finished, and the mp4 was rendered when render was set.
  • video.failed — generation or rendering failed at some point in the pipeline.

One exception: rendering a finished video sends a second event once the mp4 lands, since that export is a new thing to wait for. Changes made in the editor, including re-exporting there, produce no events.

Subscribing

Pass webhookUrl in the body of any create endpoint, such as POST /v1/ai-videos. The URL is stored on the video record and used for that video only, there's no global subscription concept, every video carries its own callback. The URL must use https:// and is capped at 1024 characters.

The render endpoint takes the same field, so a video you created without a callback can still be notified when its mp4 is ready. The url you pass there replaces the stored one.

Create-video body excerpt
{
  "script": "In 1963, a small town woke up to something it could not explain...",
  "webhookUrl": "https://your-app.example.com/hooks/vsub-video"
}

Request shape

Vsub sends a JSON POST. The body carries the same video object you get from Get a video, so a completed render arrives with a ready to use videoUrl.

Webhook request
POST https://your-app.example.com/hooks/vsub-video
Content-Type: application/json
X-Vsub-Event: video.completed
X-Vsub-Video-Id: 9f0c1d5e-3a1b-4c2f-9d7e-8b1a2c3d4e5f

{
  "event": "video.completed",
  "video": {
    "id": "9f0c1d5e-3a1b-4c2f-9d7e-8b1a2c3d4e5f",
    "status": "completed",
    "step": "completed",
    "render": true,
    "createdAt": "2026-08-04T09:20:11.000Z",
    "editorUrl": "https://vsub.io/workspace/editor/9f0c1d5e-3a1b-4c2f-9d7e-8b1a2c3d4e5f",
    "videoUrl": "https://files.vsub.io/output-9f0c1d5e.mp4"
  }
}
Headers
Content-Type
string
Always application/json.
X-Vsub-Event
string
Event name, video.completed or video.failed. Additional event types may be added later, existing receivers can ignore unknown values.
X-Vsub-Video-Id
string
Mirrors body.video.id. Stable across retries, use this as your idempotency key.
Body
event
"video.completed" | "video.failed"
The terminal status that triggered the event.
video
object
The full video object, identical to the response of GET /v1/videos/:id. On failure it carries the error field explaining what went wrong.

Responding

Return any 2xx status within 10 seconds to acknowledge the delivery. Anything else, non-2xx, timeout, DNS failure, TLS error, is treated as a failure and the message is retried.

We don't inspect the response body, so an empty 200 OK is fine. Keep your handler fast: do the minimum work needed to record the notification (e.g. enqueue an internal job) and return.

Retries & delivery guarantees

Delivery is at-least-once. A failed delivery is retried with a growing delay, 10 attempts in total: after 30 seconds, then 2, 10 and 30 minutes, then 1, 2, 4, 8 and 8 hours. The tries start close together so a receiver that only blipped hears about the video right away, and stretch to just under 24 hours in total, after which the event is dropped and will not be retried. Delays are measured from the end of the failed attempt, so a receiver that times out rather than answering stretches the window slightly further.

To recover from an extended outage, call Get a video for any video you remember creating, the record holds the same status information indefinitely.

Because a retry can land after you already handled the event, dedupe on X-Vsub-Video-Id. Once you've processed an event for a given video id, ignore subsequent deliveries for the same id.

Authenticating the request

Vsub currently does not sign webhook bodies. To verify the request is genuine, embed a secret token directly in your webhookUrl path or query string and check it on receipt:

Authenticated URL pattern
"webhookUrl": "https://your-app.example.com/hooks/vsub-video?secret=YOUR_SHARED_SECRET"

Treat the URL itself as a credential, rotate it if you suspect it has leaked.

Local development

Plain http:// URLs are rejected when the video is created, so a local server on http://localhost:3000 won't work directly. Use a tunneling tool such as ngrok or Cloudflare Tunnel to expose your local handler over HTTPS while developing.