Guides

Webhooks

Skip the poll loop. Pass a webhook_url on POST /v1/ads/generate and we'll POST the delivery payload to your endpoint the moment it's ready.

Configuring a webhook

Webhooks are configured per-request, not per-account. Include webhook_url on the generate call:

{
  "count": 3,
  "format": "full",
  "webhook_url": "https://yourapp.com/dessert-callback"
}

Your endpoint must be reachable over HTTPS and respond with a 2xx status within 10 seconds. Anything else is treated as a retriable failure.

Event types

EventWhen it fires
delivery.completedAll ads in the delivery are rendered. Payload includes the same shape as GET /v1/deliveries/:id.
delivery.failedThe orchestrator gave up after retries. Credits are refunded automatically.

Payload shape

{
  "event": "delivery.completed",
  "delivery_id": "recXXXXXXXXXXXXXX",
  "request_id": "req_a1b2c3d4e5f6789012345abc",
  "status": "completed",
  "ad_count": 6,
  "ads": [
    {
      "ad_id": "recAAAAAAAAAAAAAA",
      "orientation": "vertical",
      "headline": "Glass skin, on the first morning",
      "static_url": "https://cdn.dessert.dev/.../ad-1-vrt.png",
      "video_url":  "https://cdn.dessert.dev/.../ad-1-vrt.mp4"
    }
  ],
  "viewer_url": "https://app.dessert.dev/viewer/recXXXXXXXXXXXXXX",
  "delivered_at": "2026-05-19T18:31:47.000Z"
}

Signature verification

Every webhook ships with an X-Dessert-Signature header — an HMAC-SHA256 of the raw request body, signed with your account's webhook secret, encoded as lowercase hexadecimal. Verify it on your side before trusting the payload.

Find your webhook secret in Settings → Webhooks on app.dessert.dev.

Python (Flask)
TypeScript (Express)
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["DESSERT_WEBHOOK_SECRET"].encode()

@app.post("/dessert-callback")
def dessert_callback():
    sig = request.headers.get("X-Dessert-Signature", "")
    expected = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(401)

    body = request.get_json()
    # handle body["event"] / body["ads"] …
    return "", 200
import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.DESSERT_WEBHOOK_SECRET!;

// Important: capture the raw body for HMAC verification.
app.use(express.raw({ type: "application/json" }));

app.post("/dessert-callback", (req, res) => {
  const sig = req.header("X-Dessert-Signature") || "";
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(req.body)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }

  const body = JSON.parse(req.body.toString("utf8"));
  // handle body.event / body.ads …
  res.status(200).end();
});
Always use the raw bytes Re-serializing the parsed JSON before HMAC verification will produce a different byte sequence and the signature won't match. Hash the request body exactly as it arrived on the wire.

Retry behavior

Webhooks are dispatched through Google Cloud Tasks. If your endpoint returns a non-2xx status or times out (10 second budget), we retry up to 3 times with exponential backoff (≈30s, 5min, 30min). After the third failure the event is dropped.

Your handler should be idempotent: if you've already processed a delivery_id + event pair, return 200 immediately and skip the work.

Testing locally

Use a tunneling tool like ngrok or cloudflared to expose your local server, then pass the public URL as webhook_url on a test generate call.