Endpoints

Generate ads

POST https://api.dessert.dev/v1/ads/generate Try it →

The main generation endpoint. Submit a creative brief and Dessert returns a delivery_id within milliseconds. The actual ads — copy, rendered statics, animated MP4s — come back through the status endpoint or your webhook a few minutes later.

Every request creates one delivery containing count ad concepts. Each concept is rendered in two aspect ratios (vertical 1080×1920 and square 1080×1350), so a request with count: 3 produces six rendered files.

Headers

HeaderDescription
AuthorizationrequiredBearer dsrt_live_…
Content-Typerequiredapplication/json
Idempotency-Keyoptional A unique string (UUID recommended) that lets you retry safely. We cache the response for 24h and replay it on duplicate keys instead of generating new ads.
Preferoptional Set to wait to make this call block until the ads are ready and return them inline in a single response (Replicate-style). The connection stays open for up to 4 min; if the delivery isn't done by then you get a 202 with a status URL you can poll. Without this header, the call returns a 202 immediately and you poll GET /v1/deliveries/:id yourself.
One-shot vs. async. If you want the simplest possible integration — fire one curl, get finished ad URLs back — use Prefer: wait. If you're calling from a long-running job and don't want to hold the connection, omit it and poll the status_url we return.

Request body

FieldDescription
countintegeroptional Number of ad concepts to generate. Defaults to 1. Each concept produces one vertical + one square render.
formatstringoptional "full" (default) — static + animated video, 15 credits per ad.
"static" — static image only, 5 credits per ad.
brand_idstringoptional The brand to generate against. Omit and we use your account's default brand. Get IDs from GET /v1/brands.
product_refsstring[]optional Restrict generation to specific products. Accepts any mix of record IDs ("rec0aBcD1234"), handles ("jet-lag-mask"), or name substrings ("jet lag"). Get the list from GET /v1/products. If omitted, Dessert rotates across all active products on the brand. Alias: product_ids still works.
creativeobjectoptional Creative direction overrides. See creative object below.
in_situbooleanoptional If true, the product is composited into a lifestyle scene via Gemini before the ad is rendered. Defaults to false.
webhook_urlstringoptional HTTPS URL that we POST to when the delivery completes. See Webhooks.

The creative object

FieldDescription
copy_anglestringoptional Forces the headline + body angle. One of: "pain + solution", "benefit outcome", "sensory", "transformation", "ritual", "social proof". Omit to let Dessert rotate.
brand_tonestringoptional Free-form tone override for this request (e.g. "clinical authority", "playful modern"). Falls back to the brand default.
animation_promptsstring[]optional Pin the animation styles to a specific subset. Pass one or more of the 36 production Animation Prompt categories — see the styles reference for the full list.
style_searchstringoptional Natural-language description of the look you want (e.g. "warm honey drip on amber, minimal, slow"). Used only when animation_prompts is empty. Behind the scenes we run an embedding search and pin the top matches as featured styles.
Two ways to constrain style animation_prompts is categorical and exact — use it when you know the named prompt (e.g. "Serum Droplets"). style_search is semantic — use it when you want to describe a vibe. If you set both, animation_prompts wins.

Request examples

A minimal call using only your defaults:

curl
Python
TypeScript
MCP
curl https://api.dessert.dev/v1/ads/generate \
  -H "Authorization: Bearer $DESSERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "count": 3,
    "format": "full",
    "creative": {
      "copy_angle": "sensory",
      "brand_tone": "clinical authority",
      "animation_prompts": ["Serum Droplets", "Lotion Texture"]
    },
    "webhook_url": "https://yourapp.com/dessert-callback"
  }'
import os, requests

r = requests.post(
    "https://api.dessert.dev/v1/ads/generate",
    headers={
        "Authorization": f"Bearer {os.environ['DESSERT_API_KEY']}",
        "Idempotency-Key": "campaign-q2-2026-batch-01",
    },
    json={
        "count": 3,
        "format": "full",
        "creative": {
            "copy_angle": "sensory",
            "brand_tone": "clinical authority",
            "animation_prompts": ["Serum Droplets", "Lotion Texture"],
        },
        "webhook_url": "https://yourapp.com/dessert-callback",
    },
    timeout=30,
)
r.raise_for_status()
delivery = r.json()
const res = await fetch("https://api.dessert.dev/v1/ads/generate", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.DESSERT_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "campaign-q2-2026-batch-01",
  },
  body: JSON.stringify({
    count: 3,
    format: "full",
    creative: {
      copy_angle: "sensory",
      brand_tone: "clinical authority",
      animation_prompts: ["Serum Droplets", "Lotion Texture"],
    },
    webhook_url: "https://yourapp.com/dessert-callback",
  }),
});
const delivery = await res.json();
{
  "tool": "dessert.generate_ads",
  "arguments": {
    "count": 3,
    "format": "full",
    "creative": {
      "copy_angle": "sensory",
      "brand_tone": "clinical authority",
      "animation_prompts": ["Serum Droplets", "Lotion Texture"]
    },
    "webhook_url": "https://yourapp.com/dessert-callback"
  }
}

Response — async (no Prefer header)

Returns 202 Accepted immediately. The delivery is queued; poll status_url for completion.

FieldDescription
delivery_idstringOpaque identifier for this delivery. Pass to GET /v1/deliveries/:id.
statusstring"queued" when async.
request_idstringTracing ID for support — include in any bug reports.
ad_count_expectedintegerNumber of ad concepts that will be generated (= your count).
formatstringEcho of the format you requested.
credits_balanceintegerRemaining credit balance after this call's deduction.
status_urlstringFull URL to poll for delivery progress.
eta_secondsintegerRough completion estimate (180s for static, 480s for full).
replayed_from_idempotency_keybooleanPresent and true only when an Idempotency-Key matched a prior request.
{
  "delivery_id": "recXXXXXXXXXXXXXX",
  "status": "queued",
  "request_id": "req_a1b2c3d4e5f6789012345abc",
  "ad_count_expected": 3,
  "format": "full",
  "credits_balance": 205,
  "status_url": "https://api.dessert.dev/v1/deliveries/recXXXXXXXXXXXXXX",
  "eta_seconds": 480
}

Response — blocking (Prefer: wait)

Returns 200 OK with the finished ads inline when the delivery completes. If it's still running after ~4 min, falls back to the async shape above plus a note telling you how to resume.

FieldDescription
statusstring"completed" on success; "failed" if the delivery errored.
adsarrayOne entry per ad orientation (VRT + SQ for each concept).
ads[].ad_idstringRecord id for this individual ad.
ads[].orientationstring"vertical" (1080×1920) or "square" (1080×1350).
ads[].headlinestringAI-generated headline.
ads[].static_urlstringPublic PNG URL — the static ad.
ads[].video_urlstringPublic MP4 URL — the video ad. null when format=static.
{
  "delivery_id": "recXXXXXXXXXXXXXX",
  "status": "completed",
  "ad_count_expected": 1,
  "format": "full",
  "credits_balance": 190,
  "ads": [
    {
      "ad_id": "recAd1",
      "orientation": "vertical",
      "headline": "The vitamin C serum worth the ritual",
      "static_url": "https://storage.googleapis.com/.../recAd1_v.png",
      "video_url":  "https://storage.googleapis.com/.../recAd1_v.mp4"
    },
    {
      "ad_id": "recAd2",
      "orientation": "square",
      "headline": "Vitamin C that you can feel",
      "static_url": "https://storage.googleapis.com/.../recAd2_s.png",
      "video_url":  "https://storage.googleapis.com/.../recAd2_s.mp4"
    }
  ]
}

Errors

StatusCodeCause
400invalid_countcount was not a positive integer.
400invalid_formatformat was not "full" or "static".
400no_brand_selectedAccount has no default brand and no brand_id was provided. Create one with POST /v1/brands.
402insufficient_creditsNot enough credits and auto-recharge is off or failed. Top up.
404brand_not_foundThe brand_id does not belong to this account.
425brand_onboarding_in_progressBrand scrape is still running. Try again in 15–60 minutes.
502orchestration_failedUpstream orchestrator was unreachable. Safe to retry (with an Idempotency-Key).

See the Errors reference for the full envelope.