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
| Event | When it fires |
|---|---|
| delivery.completed | All ads in the delivery are rendered. Payload includes the same shape as GET /v1/deliveries/:id. |
| delivery.failed | The 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.
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();
});
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.