Webhooks (production patterns)
Webhooks are the recommended way to learn when a render finishes — they replace polling and let your backend react the moment a video is ready. This guide covers the production-grade patterns that go beyond the basic webhooks reference.
When to use webhooks vs polling
| Situation | Recommended |
|---|---|
| Backend service with a public HTTPS endpoint | Webhooks |
| Local script or interactive CLI tool | Polling |
| Long-running batch job (hundreds of renders / minute) | Webhooks |
| Render duration < 30 seconds and you can afford a sync wait | Polling (simpler) |
Configure a webhook destination
Add a webhook entry to the exports[].destinations array:
{
"resolution": "full-hd",
"scenes": [ /* ... */ ],
"exports": [{
"destinations": [{
"type": "webhook",
"endpoint": "https://api.example.com/json2video/done"
}]
}]
}
When the movie finishes (or fails), JSON2Video sends an HTTP POST with the rendered movie object as the body.
What the payload looks like
The payload mirrors what the GET /v2/movies?project=... endpoint returns:
{
"success": true,
"movie": {
"project": "WAEE8PohgVwv2teP",
"status": "done",
"url": "https://assets.json2video.com/clients/.../movie.mp4",
"thumbnail": "https://assets.json2video.com/clients/.../thumbnail.jpg",
"duration": 12.5,
"size": 2451234,
"client-data": { /* anything you sent in the original request */ }
}
}
The client-data field is the most important field for production. Set it on the original POST /v2/movies to anything that helps your backend identify which business object this render belongs to (an order ID, a user ID, a campaign slug, a row ID in your database). It is echoed back verbatim.
Receiving webhooks safely
Your endpoint must:
- Be publicly reachable over HTTPS. Self-signed certs are not accepted; use a real CA (Let's Encrypt, Cloudflare, etc.).
- Respond quickly. Aim for
< 5 seconds. If you need to do heavy work, queue it and return immediately. - Be idempotent. The same movie payload may arrive more than once if your endpoint times out. De-duplicate on
movie.project. - Tolerate unknown fields. New fields may be added over time; your parser should not reject them.
A minimal Node/Express handler:
import express from "express";
const app = express();
app.use(express.json({ limit: "1mb" }));
app.post("/json2video/done", async (req, res) => {
res.status(200).end(); // ack first, work after
const { movie } = req.body;
if (!movie?.project) return;
// Lookup the order in your DB by movie.client-data.orderId
await handleRender(movie);
});
Error handling
If the render fails, JSON2Video still calls your webhook. Check movie.status:
"done"— render succeeded;movie.urlis set."error"— render failed;movie.messagedescribes why."timeout"— render exceeded the maximum allowed time.
Handle the error states explicitly in your handler — don't assume success: true at the top level means the video is ready.
Local development with webhooks
Public webhook receivers are hard during local development. Use:
- ngrok — tunnels a public HTTPS URL to
localhost. - Cloudflare Tunnel — free, no time-out.
- webhook.site — quick request inspection without writing any code.