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 whenrenderwas 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.
{
"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.
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"
}
}Content-TypeX-Vsub-EventX-Vsub-Video-IdeventvideoResponding
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:
"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.
