REST API

Webhooks

Get push-based updates when a video completes or fails. Avoid polling.

Event types

  • video.created — fires when a video is queued and credits have been held.
  • video.completed — fires once the final composed video URL is available. The outputUrl in the payload is the deliverable.
  • video.failed — fires when generation fails. Credits are refunded automatically; the payload includes error with a sanitized message.

Register an endpoint

Add a webhook at Settings → Webhooks. You'll see a signing secret once — copy it before closing the dialog. You can reveal it again any time, but rotating it requires deleting and recreating the webhook.

Production endpoints must use HTTPS. We block deliveries to private ranges (10.*, 192.168.*, loopback) — webhooks should hit your public endpoint, not your own infra.

Payload shape

Every delivery is a JSON POST with these headers:

  • X-UnrealUGC-Delivery-Id — stable id for idempotency
  • X-UnrealUGC-Event — event name
  • X-UnrealUGC-Timestamp — Unix seconds
  • X-UnrealUGC-Signature — HMAC-SHA256 hex
json
{
  "id": "wd_2NPj...",
  "event": "video.completed",
  "created": 1747657281234,
  "data": {
    "video": {
      "id": "cv1jzn4a2k...",
      "status": "COMPLETED",
      "title": "Skincare routine that just works",
      "outputUrl": "https://media.unrealugc.com/videos/.../final.mp4",
      "creditsCharged": 280,
      "creditsFinal": 280,
      ...
    }
  }
}

Verifying signatures

The signature is HMAC-SHA256 over {deliveryId}.{timestamp}.{rawBody}. Reject any request older than 5 minutes (Standard Webhooks recommendation).

javascript
import crypto from "node:crypto";

export function verifyUnrealUgcWebhook(req, rawBody, secret) {
  const id = req.headers["x-unrealugc-delivery-id"];
  const ts = req.headers["x-unrealugc-timestamp"];
  const sig = req.headers["x-unrealugc-signature"];

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${id}.${ts}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(sig, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
python
import hmac, hashlib, time

def verify(headers, raw_body, secret):
    delivery_id = headers["X-UnrealUGC-Delivery-Id"]
    ts = headers["X-UnrealUGC-Timestamp"]
    sig = headers["X-UnrealUGC-Signature"]

    if abs(time.time() - int(ts)) > 300:
        return False

    signed = f"{delivery_id}.{ts}.{raw_body}".encode()
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Retries & backoff

Non-2xx responses are retried with exponential backoff: 1m → 5m → 30m → 2h → 12h → 24h (~6 attempts, ~38h total before giving up).

We auto-disable a webhook after 20 consecutive failures. The dashboard shows the failure counter so you can fix the endpoint and re-enable it. Re-enabling resets the counter.

Make your handler idempotent — we may deliver the same event more than once. Use X-UnrealUGC-Delivery-Id as the dedupe key.