Generate ads
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
| Header | Description |
|---|---|
| Authorizationrequired | Bearer dsrt_live_… |
| Content-Typerequired | application/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.
|
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
| Field | Description |
|---|---|
| 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
| Field | Description |
|---|---|
| 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.
|
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 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.
| Field | Description |
|---|---|
| delivery_idstring | Opaque identifier for this delivery. Pass to GET /v1/deliveries/:id. |
| statusstring | "queued" when async. |
| request_idstring | Tracing ID for support — include in any bug reports. |
| ad_count_expectedinteger | Number of ad concepts that will be generated (= your count). |
| formatstring | Echo of the format you requested. |
| credits_balanceinteger | Remaining credit balance after this call's deduction. |
| status_urlstring | Full URL to poll for delivery progress. |
| eta_secondsinteger | Rough completion estimate (180s for static, 480s for full). |
| replayed_from_idempotency_keyboolean | Present 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.
| Field | Description |
|---|---|
| statusstring | "completed" on success; "failed" if the delivery errored. |
| adsarray | One entry per ad orientation (VRT + SQ for each concept). |
| ads[].ad_idstring | Record id for this individual ad. |
| ads[].orientationstring | "vertical" (1080×1920) or "square" (1080×1350). |
| ads[].headlinestring | AI-generated headline. |
| ads[].static_urlstring | Public PNG URL — the static ad. |
| ads[].video_urlstring | Public 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
| Status | Code | Cause |
|---|---|---|
| 400 | invalid_count | count was not a positive integer. |
| 400 | invalid_format | format was not "full" or "static". |
| 400 | no_brand_selected | Account has no default brand and no brand_id was provided. Create one with POST /v1/brands. |
| 402 | insufficient_credits | Not enough credits and auto-recharge is off or failed. Top up. |
| 404 | brand_not_found | The brand_id does not belong to this account. |
| 425 | brand_onboarding_in_progress | Brand scrape is still running. Try again in 15–60 minutes. |
| 502 | orchestration_failed | Upstream orchestrator was unreachable. Safe to retry (with an Idempotency-Key). |
See the Errors reference for the full envelope.