Webhooks
Webhooks deliver render results to a URL of your choice. They are the recommended alternative to polling GET /v2/movies.
Configuration
Webhooks are declared in the movie's exports array as a destinations[] entry with type: "webhook", or by referencing a webhook saved in Dashboard → Connections (Output destinations tab).
Inline endpoint
{
"resolution": "full-hd",
"scenes": [],
"exports": [
{
"destinations": [
{
"type": "webhook",
"endpoint": "https://example.com/webhook"
}
]
}
]
}
| Field | Required | Description |
|---|---|---|
type |
yes | "webhook". |
endpoint |
yes | Publicly reachable URL. Use https://. |
content-type |
no | application/json (default) or application/x-www-form-urlencoded. The full MIME string is required — short values like json / urlencoded are not recognized and fall back to JSON. |
Saved webhook
Save the endpoint once in Dashboard → Connections and reference it by ID. A saved webhook is just the endpoint URL: JSON2Video sends no custom or authentication headers.
{
"exports": [
{ "destinations": [ { "id": "your-webhook-connection-id" } ] }
]
}
The dashboard only saves https:// URLs. Webhooks saved earlier with http:// still receive deliveries.
When the webhook is sent
The webhook is sent once per render, after the render finishes:
- Render succeeded — right after the video is uploaded, with
success: true. - Render failed — shortly after the failure is detected, with
success: falseand anerrormessage. This also applies to renders that failed before producing any video.
There is no event name in the payload: treat any POST to your URL as "a render finished" and read success.
Payload
The request is a POST with a flat body (no nested movie object). By default the body is JSON (Content-Type: application/json); with content-type: application/x-www-form-urlencoded the same fields are sent form-encoded.
Successful render:
{
"width": 1920,
"height": 1080,
"codec": "h264",
"codec_type": "video",
"pix_fmt": "yuv420p",
"duration": 10.5,
"size": 4567890,
"video_streams": 1,
"audio_streams": 1,
"frame_rate": 25,
"color_transfer": null,
"color_primaries": null,
"color_space": null,
"url": "https://assets.json2video.com/clients/xxxxxxxx/renders/yourmovie.mp4",
"thumbnail": "https://assets.json2video.com/clients/xxxxxxxx/renders/yourmovie.jpg",
"success": true,
"project": "JkGxEoPRF9EgRb32",
"id": "your-movie-id",
"client-data": {
"order_id": "ord_42"
}
}
Failed render:
{
"success": false,
"error": "Scene #1, element #2: …",
"project": "JkGxEoPRF9EgRb32",
"id": "your-movie-id",
"client-data": {
"order_id": "ord_42"
}
}
| Field | Type | Description |
|---|---|---|
success |
boolean | true when the render produced a video. |
error |
string | Why the render failed. Only when success is false. |
project |
string | The 16-character project ID. |
id |
string | The id from the submitted Movie JSON. Omitted when the movie has none. |
client-data |
object | The client-data from the submitted Movie JSON ({} when the movie has none). |
url |
string | Public URL of the rendered MP4. On a failed render it is absent or null. |
thumbnail |
string | Public URL of the movie thumbnail. Omitted when the render produced none, so check for its presence. See the movie thumbnail property to choose the frame. |
width, height |
number | Output dimensions in pixels. Successful renders only. |
duration |
number | Output duration in seconds. Successful renders only. |
size |
number | Output file size in bytes. Successful renders only. |
codec, codec_type, pix_fmt, video_streams, audio_streams, frame_rate, color_transfer, color_primaries, color_space |
Technical details of the output file. Successful renders only. |
With
application/x-www-form-urlencoded, every value arrives as a string. Tolerate unknown fields: new ones may be added.
Test requests
The Test button in Dashboard → Connections sends one request shaped like a successful delivery, with fake values (project and id are json2video-test, URLs point at example.com) and an extra field "test": true. Receivers can ignore requests with "test": true.
Delivery and retries
- JSON2Video waits up to 30 seconds for your answer. Any
2xxstatus counts as delivered. - Retried — at most 2 more times, after 1 second and then 3 seconds — only when your server answered with a
5xxstatus, or when the connection failed before any answer (DNS error, connection refused or reset, TLS error). - Not retried: no answer within 30 seconds (your server may still be processing the request, so it is not sent again),
3xxand4xxstatuses. Redirects are not followed: use the final URL. - JSON2Video only connects to public internet addresses. An endpoint that resolves to a private, loopback, link-local or reserved address is never called (
Not delivered: … resolves to a private, loopback or link-local network address. JSON2Video only connects to public internet addresses.).
Because a 5xx answer or a reset connection is retried, make your receiver idempotent (de-duplicate on project), answer within a few seconds, and do heavy work after answering.
Delivery results
Every delivery is recorded on the movie:
- In the dashboard: Render logs → the render → Deliveries, and the Last delivery column of Connections → Output destinations for saved destinations.
- In the API:
GET /v2/movies?project={id}returnsmovie.destinations_result— one entry per destination, in order (nullwhen nothing was delivered). See Get movie status.
{
"destinations_result": [
{
"index": 0,
"type": "webhook",
"id": "my-webhook",
"status": "ok",
"http_status": 200,
"attempts": 1,
"message": "Delivered to https://example.com (HTTP 200).",
"duration_ms": 412,
"at": "2026-09-17T10:15:02.114Z"
}
]
}
Messages show URLs as scheme and host only, and never contain passwords or the path and query of your endpoint.
Verification
Webhooks are not signed by JSON2Video, and saved webhooks cannot add headers. To verify authenticity:
- Use a long, unguessable path in your endpoint URL (e.g.
https://your.app/webhooks/json2video/abc123…). - Optionally include a secret token in the URL query string or path, and check it server-side.
- Cross-check the payload by calling
GET /v2/movies?project={id}with your API key.
Multiple destinations
Destinations inside exports[].destinations run in order, one after the other. A failing destination does not stop the next ones. A common pattern is to upload the video to an FTP server first, then notify your backend with a webhook:
{
"exports": [
{
"destinations": [
{ "id": "your-ftp-connection-id" },
{ "type": "webhook", "endpoint": "https://example.com/webhook" }
]
}
]
}
Use a single object in exports (with as many destinations as you need): a movie with more than one export object delivers nothing and records an error.
Building the receiver
The endpoint must be publicly reachable, ideally over HTTPS with a valid certificate. Minimal receivers:
PHP
<?php
$payload = json_decode(file_get_contents("php://input"), true);
if (!empty($payload["test"])) {
http_response_code(200);
exit;
}
$projectId = $payload["project"] ?? null;
if (!empty($payload["success"])) {
$videoUrl = $payload["url"];
// Your business logic here.
} else {
$error = $payload["error"] ?? "";
// Handle the failed render.
}
http_response_code(200);
echo "ok";
Node.js (Express)
const express = require("express");
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
const { success, url, error, project, test } = req.body;
res.status(200).send("ok"); // answer first
if (test) return;
// Your business logic here (de-duplicate on `project`).
});
app.listen(3000);
Always respond with a 2xx status code as soon as the payload is durably captured. Heavy work should happen out of band.