Webhooks
Webhooks deliver render completion events 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[].type = "webhook" entry, or by referencing a Dashboard connection ID.
Inline endpoint
{
"resolution": "full-hd",
"scenes": [],
"exports": [
{
"destinations": [
{
"type": "webhook",
"endpoint": "https://example.com/webhook",
"content-type": "json"
}
]
}
]
}
| Field | Required | Description |
|---|---|---|
type |
yes | "webhook". |
endpoint |
yes | Publicly reachable HTTPS URL. |
content-type |
no | json (default) or urlencoded. |
Connection reference
For security, store the endpoint as a Dashboard connection and reference it by ID. This keeps secrets out of the Movie JSON.
{
"exports": [
{ "destinations": [ { "id": "your-webhook-connection-id" } ] }
]
}
Event types
The webhook fires once per export pipeline, after rendering completes (or, when configured, after another destination has finished). There is one event today:
| Event | Fired when |
|---|---|
movie.completed |
The render finished — successfully or with an error. Inspect success and status in the payload to distinguish. |
The event name is not currently transmitted in the payload; consumers should treat any inbound POST to their webhook URL as a movie.completed event.
Payload
The HTTP request is POST with the body shape below. Default content-type is application/json. When the destination declares content-type: urlencoded the same fields are sent as application/x-www-form-urlencoded.
{
"width": "1920",
"height": "1080",
"duration": "10.5",
"size": "4567890",
"url": "https://assets.json2video.com/clients/xxxxxxxx/renders/yourmovie.mp4",
"project": "JkGxEoPRF9EgRb32",
"id": "your-movie-id",
"client-data": {
"order_id": "ord_42"
}
}
| Field | Type | Description |
|---|---|---|
width |
string | Output width in pixels. |
height |
string | Output height in pixels. |
duration |
string | Output duration in seconds. |
size |
string | Output file size in bytes. |
url |
string | Public URL of the rendered MP4. |
project |
string | The 16-character project ID. |
id |
string | The id from the submitted Movie JSON, if any. |
client-data |
object | The client-data from the submitted Movie JSON, if any. |
Field types are reported as strings in the current payload format. Validate accordingly.
Failure cases
When the render fails, the webhook is still fired (assuming an export pipeline ran). The payload may have an empty or partial url field. Consumers should always cross-check the final status by calling GET /v2/movies?project={id} when handling a webhook.
Retries
The exact retry behaviour is subject to change. Confirm with support if your workflow depends on guaranteed delivery. The handler treats the destination as a fire-and-forget HTTP POST: it is sent once, with a short request timeout, and any non-2xx response is recorded but not retried automatically by the public API today.
For workflows that require strong delivery guarantees, configure your endpoint behind a queue you control (e.g. Make.com, n8n, or your own job runner) and reconcile against
GET /v2/movieson a schedule.
Verification
Webhooks are not currently signed by JSON2Video. To verify authenticity:
- Configure 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 as part of the path, and check it server-side.
- Always cross-check the payload by calling
GET /v2/movies?project={id}with your API key.
Multiple destinations
Destinations inside an exports[].destinations array run sequentially. 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" }
]
}
]
}
Building the receiver
The endpoint must be publicly reachable over HTTPS with a valid certificate. Minimal receivers:
PHP
<?php
$payload = json_decode(file_get_contents("php://input"), true);
$videoUrl = $payload["url"] ?? null;
$projectId = $payload["project"] ?? null;
// Your business logic here.
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 { url, project } = req.body;
// Your business logic here.
res.status(200).send("ok");
});
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.