Archived docs Get your API Key
Get started
Tutorials
Guides
Reference
Help for coding agents
๐Ÿค– AI Assistant

15. Webhooks

Polling GET /v2/movies?project=โ€ฆ is fine for scripts. Production apps prefer to be notified when a render finishes. This chapter adds a webhook destination to the listing โ€” when the video is ready, JSON2Video POSTs a JSON payload to your URL with the result.

Prerequisites: chapter 14. You should have a public HTTPS endpoint that can receive a POST (use webhook.site for quick testing).

Step 1 โ€” Add a webhook destination

Webhook destinations live in the top-level exports array. Each export item has a destinations array; each destination has a type and the fields that type requires.

{
  "exports": [
    {
      "destinations": [
        {
          "type": "webhook",
          "endpoint": "https://your-app.example/json2video-callback"
        }
      ]
    }
  ]
}

JSON2Video will POST to endpoint once when the movie finishes (success or failure).

Step 2 โ€” Include some correlation metadata

When your endpoint receives the callback it needs to know which property / listing the video belongs to. The cleanest way is client-data โ€” an arbitrary object echoed back verbatim in the callback.

{
  "client-data": {
    "listing_id": "L-4821",
    "property_address": "123 Oak Street",
    "agent_id": "AG-42"
  }
}

Drop that at movie level. Whatever you put here, you'll receive in the webhook payload.

Step 3 โ€” What the webhook receives

JSON2Video POSTs a flat JSON body when the render is done. Shape (technical fields such as codec omitted):

{
  "success":     true,
  "project":     "abc123",
  "url":         "https://assets.json2video.com/clients/.../abc123.mp4",
  "thumbnail":   "https://assets.json2video.com/clients/.../abc123.jpg",
  "width":       1920,
  "height":      1080,
  "duration":    24,
  "size":        5421988,
  "client-data": {
    "listing_id":       "L-4821",
    "property_address": "123 Oak Street",
    "agent_id":         "AG-42"
  }
}

On failure success is false, an error field says why, and there is no video URL. client-data (and the movie's id, if you set one) are sent in both cases. See Webhooks reference for the canonical payload contract.

Step 4 โ€” A minimal receiver

A receiver reads the payload and triggers your downstream logic โ€” push to a CRM, send an email, update the listing record. Answer quickly with a 2xx status: JSON2Video waits up to 30 seconds for the answer.

import express from "express";

const app = express();
app.use(express.json());

app.post("/json2video-callback", (req, res) => {
  res.status(200).send("ok"); // answer first, work after
  const { success, url, error, test, "client-data": cd = {} } = req.body;
  if (test) return; // sent by the Test button in the dashboard
  if (success) {
    console.log(`Listing ${cd.listing_id} video ready: ${url}`);
    // updateCRM(cd.listing_id, url);
  } else {
    console.error(`Listing ${cd.listing_id} failed: ${error}`);
  }
});

app.listen(3000);
from flask import Flask, request

app = Flask(__name__)

@app.post("/json2video-callback")
def callback():
    body = request.get_json()
    if body.get("test"):
        return "ok", 200
    cd = body.get("client-data", {})
    if body.get("success"):
        print(f"Listing {cd.get('listing_id')} ready: {body['url']}")
    else:
        print(f"Listing {cd.get('listing_id')} failed: {body.get('error')}")
    return "ok", 200
<?php
$body = json_decode(file_get_contents("php://input"), true);
$cd   = $body["client-data"] ?? [];
if (empty($body["test"])) {
    if (!empty($body["success"])) {
        error_log("Listing {$cd['listing_id']} ready: {$body['url']}");
    } else {
        error_log("Listing {$cd['listing_id']} failed: {$body['error']}");
    }
}
http_response_code(200);
echo "ok";

Step 5 โ€” Save the webhook in the dashboard (optional)

Instead of repeating the URL in every movie, save it once in Dashboard โ†’ Connections (Output destinations โ†’ Add destination โ†’ Webhook) and reference it by id:

{
  "exports": [
    {
      "destinations": [
        { "id": "my-app-webhook" }
      ]
    }
  ]
}

A saved webhook is only the URL (it must start with https://); JSON2Video sends no custom headers. Use Test in the dashboard to send a request with "test": true to your receiver before your first render.

The complete final JSON

{
  "resolution": "full-hd",
  "client-data": {
    "listing_id": "L-4821",
    "property_address": "123 Oak Street",
    "agent_id": "AG-42"
  },
  "exports": [
    {
      "destinations": [
        {
          "type": "webhook",
          "endpoint": "https://your-app.example/json2video-callback"
        }
      ]
    }
  ],
  "variables": {
    "address": "123 Oak Street",
    "open_house": true,
    "rooms": [
      { "name": "Exterior",       "image": "https://cdn.json2video.com/assets/images/sample-house-front.jpg" },
      { "name": "Chef's Kitchen", "image": "https://cdn.json2video.com/assets/images/sample-house-kitchen.jpg" },
      { "name": "Master Bedroom", "image": "https://cdn.json2video.com/assets/images/sample-house-bedroom.jpg" }
    ]
  },
  "elements": [
    {
      "type": "audio",
      "src": "https://cdn.json2video.com/assets/audios/uplifting-corporate.mp3",
      "volume": 0.4
    },
    {
      "type": "voice",
      "text": "Welcome to {{ address }}.",
      "voice": "en-US-EmmaMultilingualNeural",
      "start": 1.5
    },
    {
      "type": "subtitles",
      "language": "en",
      "settings": {
        "style": "boxed-word",
        "font-family": "Inter",
        "font-size": 90,
        "word-color": "#FFFFFF",
        "line-color": "#FFFFFF",
        "position": "bottom-center",
        "all-caps": true,
        "box-color": "#0E7C66"
      }
    }
  ],
  "scenes": [
    {
      "duration": 4,
      "elements": [
        {
          "type": "component",
          "component": "basic/000",
          "settings": { "headline": "FOR SALE", "subline": "{{address}}" }
        }
      ]
    },
    {
      "duration": 4,
      "transition": { "style": "fade", "duration": 0.5 },
      "iterate": "rooms",
      "elements": [
        { "type": "image", "src": "{{ image }}" },
        { "type": "text", "text": "{{ name }}", "position": "top-left", "x": 60, "y": 60 }
      ]
    }
  ]
}

Expected output

Same chapter-13/14-style listing, but submitting the movie no longer needs polling. A few minutes after POST /v2/movies returns, your endpoint receives the JSON payload with the video URL and your client-data. Sample render: tutorial-15.mp4 (placeholder).

What you learned

  • exports[].destinations[] controls where the finished video goes.
  • A webhook destination POSTs a JSON payload to endpoint.
  • client-data is echoed back in the payload โ€” use it to correlate the callback with your domain objects.
  • A webhook saved in Dashboard โ†’ Connections is referenced by id, so the URL stays out of your movie JSON.

Going further

A webhook is retried (up to 2 more times) only when your receiver answers with a 5xx status or the connection fails before any answer; an answer that takes longer than 30 seconds is not retried. Design your receiver to be idempotent (de-duplicate on project). To see whether a delivery worked, open the render in Render logs (section Deliveries) or read destinations_result from GET /v2/movies?project=โ€ฆ. See the Webhooks reference for the full delivery contract.

Previous chapter / Next chapter

โ† 14. Conditions ยท 16. Optimization & cost โ†’