Archived docs Get your API Key
Get started
Tutorials
Guides
Reference
Help for AI 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"
        }
      ]
    }
  ]
}

The renderer 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 JSON body when the render is done. Shape:

{
  "project":     "abc123",
  "status":      "done",
  "url":         "https://assets.json2video.com/clients/.../abc123.mp4",
  "duration":    24,
  "size":        5421988,
  "client-data": {
    "listing_id":       "L-4821",
    "property_address": "123 Oak Street",
    "agent_id":         "AG-42"
  }
}

On failure status is "error" and a message field replaces url. See Webhooks reference for the canonical payload contract.

Step 4 โ€” A minimal receiver

A receiver verifies the payload and triggers your downstream logic โ€” push to a CRM, send an email, update the listing record.

import express from "express";

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

app.post("/json2video-callback", (req, res) => {
  const { project, status, url, "client-data": cd } = req.body;
  if (status === "done") {
    console.log(`Listing ${cd.listing_id} video ready: ${url}`);
    // updateCRM(cd.listing_id, url);
  } else {
    console.error(`Listing ${cd.listing_id} failed: ${req.body.message}`);
  }
  res.status(200).send("ok");
});

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

app = Flask(__name__)

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

Step 5 โ€” Use Dashboard connections for headers

If your endpoint needs an authentication header (e.g. an HMAC secret), create a Dashboard webhook connection and reference it by id instead of inlining the endpoint:

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

The connection stores the endpoint + custom headers; you don't have to ship secrets through the API payload.

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,
        "font-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",
      "iterate-as": "room",
      "elements": [
        { "type": "image", "src": "{{ room.image }}" },
        { "type": "text", "text": "{{ room.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.
  • Connections let you store endpoint + auth headers in the Dashboard instead of inlining secrets.

Going further

Webhooks today are best-effort โ€” there are no automatic retries. Design your receiver to be idempotent and to log failures so you can re-poll if needed. See the Webhooks reference for the current delivery contract.

Previous chapter / Next chapter

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