Skip to main content
ppt.video delivers task events through webhooks so you can react to video and image generation results without polling.

Why use webhooks

Webhooks push status changes to your backend the moment a task finishes, fails, or emits progress. This reduces API polling, lowers latency to downstream systems, and keeps processing aligned with your own retry and monitoring logic.

Prerequisites

  • A webhook secret generated in the ppt.video dashboard (used for HMAC verification)
  • An HTTPS endpoint that can accept POST requests and return a fast 2xx
  • Secure storage for the secret; do not embed it in client-side code

Register your webhook URL

Provide the callback URL when you create a task. Set webhook directly in the request body so ppt.video can reach your endpoint.
{
  "prompt": "<your_prompt>",
  "webhook": "https://your.app/api/pptvideo/webhook"
}
Required fields vary by model (for example, Kling, Google Veo, OpenAI Sora, Runway, Hailuo AI, Vidu AI, Luma AI, Pika AI, PixVerse AI, Wan Video, Hunyuan). Use the corresponding OpenAPI schema for required parameters.

Verify webhook signatures

Each delivery includes headers you must validate before trusting the payload (replace with the exact header names from your dashboard or OpenAPI schema):
  • X-Webhook-Id: unique delivery identifier
  • X-Webhook-Timestamp: Unix timestamp (seconds) when ppt.video sent the event
  • X-Webhook-Signature: Base64-encoded HMAC-SHA-256 signature

Signature algorithm (HMAC-SHA-256)

  1. Read the raw request body as a string.
  2. Build the signed content: <webhook_id>.<webhook_timestamp>.<body>.
  3. Base64-decode your webhook secret.
  4. Compute HMAC-SHA-256 over the signed content using the decoded secret.
  5. Base64-encode the digest and compare it to X-Webhook-Signature using a timing-safe comparison.

JavaScript example

import crypto from "crypto";
import { IncomingMessage, ServerResponse } from "http";

function handleWebhook(req: IncomingMessage, res: ServerResponse) {
  const webhookId = req.headers["x-webhook-id"] as string;
  const webhookTimestamp = req.headers["x-webhook-timestamp"] as string;
  const signature = req.headers["x-webhook-signature"] as string;

  let body = "";
  req.on("data", (chunk) => {
    body += chunk;
  });

  req.on("end", () => {
    const secret = process.env.PPTVIDEO_WEBHOOK_SECRET ?? "";
    const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;

    const computedSigBase64 = crypto
      .createHmac("sha256", Buffer.from(secret, "base64"))
      .update(signedContent)
      .digest("base64");

    const isValid =
      signature &&
      crypto.timingSafeEqual(
        Buffer.from(signature, "base64"),
        Buffer.from(computedSigBase64, "base64")
      );

    if (!isValid) {
      res.writeHead(400);
      return res.end("invalid signature");
    }

    const event = JSON.parse(body);
    // Process task event by ID and status
    res.writeHead(200);
    res.end("ok");
  });
}

Python example

import base64
import hashlib
import hmac
import json
from http.server import BaseHTTPRequestHandler, HTTPServer


class WebhookHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        webhook_id = self.headers.get("X-Webhook-Id", "")
        webhook_ts = self.headers.get("X-Webhook-Timestamp", "")
        signature = self.headers.get("X-Webhook-Signature", "")

        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length).decode("utf-8")

        secret = base64.b64decode(
            # Store securely; do not hardcode in production.
            "REPLACE_WITH_BASE64_SECRET"
        )
        signed_content = f"{webhook_id}.{webhook_ts}.{body}"

        computed = hmac.new(
            secret, signed_content.encode("utf-8"), hashlib.sha256
        ).digest()
        computed_b64 = base64.b64encode(computed).decode("utf-8")

        if not hmac.compare_digest(signature, computed_b64):
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"invalid signature")
            return

        event = json.loads(body)
        # Handle event by taskId and status
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"ok")


if __name__ == "__main__":
    HTTPServer(("0.0.0.0", 8080), WebhookHandler).serve_forever()

Event payload

Event shapes vary by model; consult the model’s OpenAPI schema. A typical delivery looks like:
{
  "id": "<string>",
  "status": "succeeded | failed",
  "payload": {},
  "meta": {},
  "data": {},
  "error": "<string|null>"
}
Use id to correlate with the original request and status to branch your business logic. payload, meta, and data are model-dependent; error is present on failure.

Response and retries

  • Return HTTP 2xx as soon as the signature is validated; avoid long-running work in the handler.
  • ppt.video will retry failed deliveries with backoff. Confirm the retry limit, schedule, and timeout in your environment (<RETRY_LIMIT>, <TIMEOUT_SECS>); design handlers to be idempotent.
  • If your endpoint is unavailable for an extended period, re-register or rotate the webhook secret and replay missed tasks as needed.