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 piece of work, 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.
  • caption_removal.completed and caption_removal.failed — how a caption removal went. A failed one is paid back, which the payload says.

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 or POST /v1/caption-remover. The URL is stored on that one record and used for it only, there's no global subscription concept, every request 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, such as video.completed. Additional event types may be added later, existing receivers can ignore unknown values.
X-Vsub-Video-Id
string
Mirrors body.video.id, on video events. Stable across retries, use this as your idempotency key.
X-Vsub-Job-Id
string
The same thing on caption removal events, mirroring body.captionRemoval.id.
Body
event
string
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. Sent on video events.
captionRemoval
object
The full removal object, identical to the response of GET /v1/caption-remover/:id. Sent on caption removal events.

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 the id header. Once you've processed an event for a given id, ignore subsequent deliveries for the same one.

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.