Waverunner API v1
Program the same always-on performance product your team uses in the app. A small REST API over those operations: JSON in, JSON out.
Authentication
Create an API key in Settings and send it as a bearer token:
curl https://waverunner.adwave.com/api/v1/campaigns \ -H "Authorization: Bearer wr_..."
Keys are scoped to the organization that was active when you created them (switch workspaces in the sidebar first if you need a key for another organization). Reads are limited to 120 requests/minute, writes to 30/minute; 429 means slow down. Errors are JSON: { "error": "..." }. Building with AI agents? The same operations are available over the MCP server.
Example: from URL to live campaign
The whole flow is five calls. Add a business and let the analysis run:
curl -X POST https://waverunner.adwave.com/api/v1/businesses \
-H "Authorization: Bearer wr_..." -H "Content-Type: application/json" \
-d '{ "url": "acme-coffee.com" }'
# → 201 { "business": { "id": "018f3a2b-...", "status": "pending" } }
# Poll until "ready" (a minute or two), then review profile + personas:
curl https://waverunner.adwave.com/api/v1/businesses/018f3a2b-... \
-H "Authorization: Bearer wr_..."Create a draft campaign. Autopilot builds a recommended creative plan after interest targeting is planned (response has generationDeferred: true), reusing ready creatives and generating only the gaps. Quote those gaps with campaignId once lines exist, check the wallet, and launch:
curl -X POST https://waverunner.adwave.com/api/v1/campaigns \
-H "Authorization: Bearer wr_..." -H "Content-Type: application/json" \
-d '{
"businessId": "018f3a2b-...",
"name": "Summer Sale",
"objective": "sales",
"dailyBudgetCents": 5000,
"offer": "Free shipping over $40"
}'
# → 201 { "campaign": { "id": "018f3c91-...", "status": "draft" },
# "campaignAdsEnqueued": false, "generationDeferred": true }
# After interest lines are planned, quote statics × lines + videos:
curl "https://waverunner.adwave.com/api/v1/creatives/quote?businessId=018f3a2b-...&campaignId=018f3c91-..." \
-H "Authorization: Bearer wr_..."
# → { "quote": { "staticCount", "interestLineCount", ... }, "campaignId": "..." }
curl https://waverunner.adwave.com/api/v1/wallet \
-H "Authorization: Bearer wr_..."
# → { "wallet": { "balanceCents": 25000, "autoRefillEnabled": false } }
curl -X POST https://waverunner.adwave.com/api/v1/campaigns/018f3c91-.../launch \
-H "Authorization: Bearer wr_..."
# → { "launched": true, "chargedDate": "2026-07-06" }From then on: GET …/report for performance, PATCH for budget changes, and POST …/lifecycle to pause, resume, or end.
Endpoints
GET/api/v1/businesses
List businesses in your organization.
{ "businesses": [{ "id", "url", "name", "status", "createdAt" }] }POST/api/v1/businesses
Add a business by website URL. Kicks off analysis (scrape → profile → personas); poll the list until status is "ready".
body: { "url": "https://example.com" }201 { "business": { "id", "status": "pending" } }GET/api/v1/businesses/{id}
Business detail: the extracted profile and every persona on the business profile with its id, Persona Reach (`targetingSeeds`: interest and demographic catalog keys), and persona-scoped demographic pins/exclusions. Pass persona ids to campaigns as icpIds (only selected personas are valid targets). After interest targeting is planned, that Reach is copied into the campaign's own flight targeting; persona seeds stay as written on the Creative profile.
{ "business": { "id", "url", "name", "status", "profile": {...}, "personas": [{ "id", "name", "description", "selected", "targetingSeeds": { "status", "interestKeys", "demographicKeys", ... } | null, "demographicPins": string[], "demographicExclusions": string[], ... }] } }POST/api/v1/businesses/{id}/archive
Archive a business that has no live or launching campaigns. Hidden from the default list; can be unarchived.
{ "ok": true, "status": "archived" }POST/api/v1/businesses/{id}/unarchive
Restore an archived business to active use.
{ "ok": true, "status": "ready" }GET/api/v1/creatives?businessId={id}&cursor={c}&limit={n}
List creatives for the business with their static variants and video ads (businessId optional; generated in campaign setup or on prior flights). Paginated: limit default 20, max 100. Distinct from a campaign's Ads set at GET /api/v1/campaigns/{id}/ads.
{ "creatives": [{ "id", "businessId", "status", "variants": [...], "videos": [...] }], "nextCursor" }GET/api/v1/creatives/quote?businessId={id}&scope={launch|all}&includeVideos={true|false}&icpId={uuid}&campaignId={uuid}
Pre-flight price for a business's ad generation batch: the per-unit breakdown, the resolved billing mode (waived = recorded free, charged = creation credit then wallet), and remaining creation credit. Defaults to the Launch pack (scope=launch: one persona + statics + social + TV). Pass campaignId for a campaign-scoped quote of the recommended creative plan gaps: statics only for interest lines that need generation across targeted personas, plus any missing video units (response includes interestLineCount; scope recommended_gaps). When interest lines are still planning, the campaign quote returns totalCents 0 with deferred: true (charge runs after interest targeting is planned). Without campaignId, library generate uses ICP-wide statics. Pass scope=all for every selected persona; includeVideos only applies then. The same planner prices the quote and the run, so the number is exact.
{ "quote": { "totalCents", "staticCount", "videoCount", "interestLineCount"?, "units": [...] }, "billing": { "mode" }, "allowanceCents", "scope", "campaignId"? }POST/api/v1/creatives/generate
Charge and enqueue a library ad generation batch for a ready business (creation credit then wallet). Quote first with GET /creatives/quote. 402 when funds are short.
body: { "businessId", "scope"?: "launch"|"all", "includeVideos"?: boolean, "icpId"?: "uuid", "instructions"?: "..." }202 { "creativeId", "enqueued": true }GET/api/v1/campaigns
List campaigns.
{ "campaigns": [{ "id", "name", "objective", "status", "dailyBudgetCents", "createdAt" }] }POST/api/v1/campaigns
Create a draft campaign. Optional geo (national | states | radius | zips | dmas | cities; preferred), states (US back-compat), platforms (open inventory always included), landingUrl, audienceBoost (needs a customer list), and measureLift for geo control. Omitted geo defaults from the business service area (or full US); omitted platforms to all enabled channels. After interest targeting is planned, Autopilot builds a recommended creative plan: reuse ready library creatives where they fit, and generate only the gaps for every targeted persona. Create does not charge generation in the create transaction: response has generationDeferred: true when ads were requested (default campaignAds: true). Pass campaignAds: false for a reuse-only plan with no generation charge; launch still requires a servable ad per targeted persona. icpIds narrows which personas the campaign selects (generation does not change that selection); omit for the lean default (1 persona under $20/day, else 2). startDate defaults to today; omit endDate to run open-ended (dated flights max 365 days). 409 business_archived when the business is archived. After launch, each channel moves from Preparing, to Starting, to Live.
body: { "businessId", "name", "objective": "awareness|traffic|leads|sales", "dailyBudgetCents": 1000..1000000, "startDate"?: "YYYY-MM-DD", "endDate"?: "YYYY-MM-DD", "landingUrl"?: "https://...", "geo"?: { "version": 2, "mode": "zips", "zips": ["78701"] }, "states"?: ["CA", ...], "platforms"?: [...], "icpIds"?: [...], "offer"?: "20% off first order", "instructions"?: "Playful tone, show real people", "campaignAds"?: true, "measureLift"?: false, "audienceBoost"?: false }201 { "campaign": { "id", "status": "draft" }, "campaignAdsEnqueued": false, "generationDeferred": true }GET/api/v1/campaigns/{id}
Campaign detail: setup, targeting, and status.
{ "campaign": { "id", "name", "objective", "status", "dailyBudgetCents", "startDate", "endDate", "landingUrl", "states", "geo", "platforms", "audienceBoost", "holdoutStates", "icpIds", "offer", "instructions", "createdAt" } }POST/api/v1/campaigns/{id}/launch
Launch a draft. When ads are already ready, charges the first day and distributes. When a creative plan is accepted but ads are still rendering, commits launch with awaitingCreatives (chargedDate null) and charges only when creatives are ready. 402 when the wallet can't cover day one; 409 when there is no creative plan and a targeted persona has no servable ad yet.
{ "launched": true, "chargedDate": "YYYY-MM-DD"|null, "awaitingCreatives"?: true }POST/api/v1/campaigns/{id}/cancel-launch
Cancel a launch that is still waiting on creatives (no day charge yet). Returns the campaign to draft. 409 when the campaign is not awaiting creatives or already charged.
{ "cancelled": true }PATCH/api/v1/campaigns/{id}
Edit a campaign: daily budget (draft/live/paused; no proration), name (any status except mid-launch; display-only), flight endDate (live/paused; JSON null clears), and/or landingUrl (live/paused refreshes serving ads; draft/error just updates the setup value). Geo (full v2 object) or states: draft/error updates setup; live/paused enqueues a geo push. Draft/error only: platforms (every enabled channel when omitted/empty), audienceBoost (stays off without a seeded customer list; use POST .../audience-boost for a loud failure instead), measureLift (recomputes the geo-holdout). 409 once launched for draft-only fields; 409 campaign_archived when the campaign is archived (restore it first). Optional Idempotency-Key header. Send at least one field.
body: { "name"?: "2..80 chars", "dailyBudgetCents"?: 1000..1000000, "endDate"?: "YYYY-MM-DD"|null, "landingUrl"?: "https://...", "geo"?: { "version": 2, "mode": "states", "states": ["CA"] }, "states"?: ["CA", ...], "platforms"?: [...], "audienceBoost"?: true, "measureLift"?: true }{ "updated": true, "name"?, "dailyBudgetCents"?, "placementsResplit"?, "endDate"?, "landingUrl"?, "versionedAdCount"?, "geo"?, "states"?, "platforms"?, "audienceBoost"?, "measureLift"? }POST/api/v1/campaigns/{id}/audience-boost
Turn Audience boost on or off (draft, live, or paused). Boost only ever targets audiences seeded from this business's own imported customer list. Turning it on without an imported customer list returns 422 seed_required, it never silently no-ops. 409 campaign_archived when the campaign is archived. Optional Idempotency-Key header.
body: { "enabled": true|false }{ "updated": true, "enabled": true|false }POST/api/v1/campaigns/{id}/retry
Retry a stalled campaign. `{"action":"launch"}` resets a failed campaign to draft and re-runs the launch gate (may defer charge while ads render); 402 when the wallet can't cover day one, 409 no_servable_ads when there is no creative plan and a targeted persona still has nothing to serve. `{"action":"failed_placements"}` re-runs distribution for a LIVE campaign's failed placements only (live ones are skipped); 409 when the campaign isn't live.
body: { "action": "launch"|"failed_placements" }{ "launched": true, "chargedDate": "YYYY-MM-DD"|null, "awaitingCreatives"?: true } | { "requeued": true }POST/api/v1/campaigns/{id}/lifecycle
Pause, resume, or end a campaign. Pausing stops future daily charges (today's stands, no proration); resuming re-charges today idempotently; ending is permanent. 202: the transition is applied by the platform workers asynchronously. 409 campaign_archived when the campaign is archived (restore it first).
body: { "action": "pause|resume|end" }202 { "requested": true, "action", "status" }POST/api/v1/campaigns/{id}/archive
Archive a campaign that is not launching or live (pause or end it first; 409 active otherwise). Hidden from the default list; launch and resume are blocked until it is restored. Ads, spend, and results are kept as a record.
{ "ok": true, "status": "archived" }POST/api/v1/campaigns/{id}/unarchive
Restore an archived campaign. Lists, launch, and resume come back; the status itself is unchanged.
{ "ok": true, "restored": true }DELETE/api/v1/campaigns/{id}
Permanently delete a draft campaign that never ran (no launch, no charges). Generated ads stay in the business Ad Library. 409 has_run once the campaign has run: archive it instead.
{ "ok": true, "deleted": true }GET/api/v1/campaigns/{id}/report
Performance report: conversions (click and view-through), sessions (ad visits from this campaign's ads), net spend, CPA, ROAS, plus per-placement and per-channel breakdowns. Funnel campaigns emit separate Mobile web and TV rows (from line-item delivery); legacy non-funnel stays one open-inventory row labeled Mobile web and TV. Includes campaign-level and per-platform CPM breakout: `cpmCents` (all-in delivered spend per 1,000 views), `audienceFeeCpmCents` (catalog list-rate estimate for attached interest/demographic audiences; not a separate wallet charge), and `mediaCpmCents` (all-in minus list-rate, floored at 0). Null CPM means spend is not reported yet or there are no views. Per-platform rows also include honest channel fields: `clickableImpressions` / `viewOnlyImpressions` (CTR uses clickable only; null on pure TV/streaming), `reach` (null when the channel has no first-party impression pixel), `clickConversions` / `viewConversions`, `assistedConversions` (distinct union of same-campaign prior touches; reporting-only, not billed), `assistedViaClick` / `assistedViaHouseholdView` / `assistedViaPersonView` (path counts that may overlap; do not sum as the headline), `assistIdentityCoverage` (campaign-level; null when no visitor-tagged conversions), `conversionShare`, `sessions`, `cpaCents`, `roas`, `roles`, optional `seedImpact` (opaque follow-on outcome metrics; omit if null), optional `includesMobileVideo` (Mobile web consideration video delivered), and optional `deviceMix` (observed devices for that surface, not separate sold channels). Omit inapplicable metrics in UIs rather than showing measured zeros. `platform` is an opaque channel id; render `platformLabel` when showing data to people. If the website's tracking tag isn't installed yet, `trackingInstalled` is false and `visitsSource` is "clicks": sessions mirror clicks and conversions can't be measured until the tag goes in. `visitsSource` "blended" means the campaign also ran before the tag was installed: `preTagClickVisits` clicks from those earlier days stay counted in sessions, and tagged sessions own the count from the install date on. `roas` is null whenever return can't be measured (no spend, no tag, or `valuesConfigured` false: no conversion has ever carried a value and no conversion rule has a per-conversion value); never a fake 0. `revenueIncludesEstimates` is true when revenue includes per-conversion values the user set on rules (render such ROAS as estimated).
{ "trackingInstalled", "visitsSource", "conversions", "sessions", "preTagClickVisits", "spendCents", "cpaCents", "valuesConfigured", "roas", "revenueIncludesEstimates", "cpmCents", "audienceFeeCpmCents", "mediaCpmCents", "assistIdentityCoverage", "byName": [...], "byPlacement": [{ "platform", "platformLabel", ... }], "byPlatform": [{ "platform", "platformLabel", "clickableImpressions", "viewOnlyImpressions", "reach", "ctr", "assistedConversions", "assistedViaClick", "assistedViaHouseholdView", "assistedViaPersonView", "conversionShare", "seedImpact", "includesMobileVideo?", "deviceMix?", "cpmCents", ... }] }GET/api/v1/campaigns/{id}/analytics?days={1..90}
Deep breakdowns for optimization: daily funnel timeseries (impressions → clicks → ad visits via `sessions` → conversions), observed device/region mix (not separate ad channels), publishers where ads appeared (`deliverySources`: TV channels, web/apps, Google networks, Meta platforms) for the selected days window, per-persona and per-targeting-segment performance, and the per-day money story (charged vs delivered vs credited). Default window 30 days. `sessions` here are ad visits from this campaign's ads (same meaning as the report).
{ "analytics": { "days", "trackingInstalled", "visitsSource", "timeseries": [{ "date", "impressions", "clicks", "sessions", "conversions" }], "deviceMix": [...], "topRegions": [...], "deliverySources": { "tvChannels": [...], "webApps": [...], "googleNetworks": [...], "metaPlatforms": [...] }, "byIcp": [...], "segments": [...], "spendSeries": [{ "date", "chargedCents", "deliveredCents", "creditedCents" }] } }GET/api/v1/campaigns/{id}/lift
Lift scorecard: causal and directional truth checks, each with a 95% confidence interval and a significance flag: geo-holdout lift (conversions where ads ran vs deliberately dark control states), cross-channel overlap (households that saw the open-inventory ad and clicked another channel vs saw-only), per-channel within-pool lift (`channels`: TV via first-party surface resolution, follow platforms via click evidence, vs a shared pool-only baseline; always directional), organic halo, and Autopilot effectiveness (acted campaigns vs observe-only comparison, with CI). `holdoutInconclusive` flags when the holdout CI still includes no lift (directional only; holdout states are not reshuffled mid-flight). Null sections mean the check isn't available for this campaign; `significant: false` means directional, not proven.
{ "lift": { "holdout": { "lift", "ciLow", "ciHigh", "pValue", "significant", "matured", "coverageSufficient", ... } | null, "overlap": { "lift", "ciLow", "ciHigh", "significant", ... }, "channels": { "exposedHouseholds", "baselineHouseholds", "baselineConverted", "minCohortHouseholds", "rows": [{ "channel", "touchKind", "touchedHouseholds", "touchedConverted", "lift", "ciLow", "ciHigh", "pValue", "significant" }] }, "halo": { "lift", "ciLow", "ciHigh", "significant", ... } | null, "optimizer": { "date", "byKind": [...], "overall": { "lift", "ciLow", "ciHigh", "significant", ... } | null }, "holdoutInconclusive": { "source": "live|audit", "auditedAt" } | null } }GET/api/v1/campaigns/{id}/customers
New vs returning customers among the campaign's attributed conversions (trailing 90 days): first-ever customers of the business vs repeat buyers, revenue for each cohort, share of conversions that were first-ever, and cost per NEW customer. Conversions with no identifiable person are counted in `unidentifiedConversions`, never silently folded into either cohort.
{ "customers": { "identifiedConversions", "unidentifiedConversions", "newConversions", "newRevenueCents", "returningConversions", "returningRevenueCents", "spendCents", "costPerNewCustomerCents", "newShare" } }GET/api/v1/businesses/{id}/ltv
Customer lifetime value for one business over the trailing 365 days: identified customers, total revenue, average LTV, quartile thresholds, and acquisition-month payback cohorts (customers, revenue accumulated to date, revenue per customer).
{ "ltv": { "customers", "totalRevenueCents", "avgLtvCents", "p25Cents", "p50Cents", "p75Cents", "cohorts": [{ "month", "customers", "revenueCents", "revenuePerCustomerCents" }] } }GET/api/v1/recommendations
Pending cross-campaign budget recommendations: moves suggested when automatic portfolio moves are off, each with both campaigns, the daily amount, and supporting evidence.
{ "recommendations": [{ "id", "businessId", "fromCampaignId", "fromCampaignName", "toCampaignId", "toCampaignName", "amountCents", "detail", "createdAt" }] }POST/api/v1/recommendations/{id}
Resolve one pending recommendation. `{"action":"apply"}` moves the daily budget through the same validated path as a manual budget edit (both campaign changes commit together or not at all, DSP pushes queued transactionally); `{"action":"dismiss"}` resolves it without moving money. Apply returns 409 when the campaigns' budgets changed since the move was computed.
{ "ok": true, "status": "accepted" | "dismissed" }GET/api/v1/campaigns/{id}/ads
The campaign's Ads set (snapshot of ads selected to deliver) with per-ad lifetime performance: status, persona, funnel stage, creative label and preview, impressions, clicks, spend, first-party conversions, revenue. Distinct from the business creative library.
{ "ads": [{ "campaignAdId", "kind", "status", "icpName", "label", "impressions", "clicks", "spendCents", "conversions", "revenueCents", ... }] }PATCH/api/v1/campaigns/{id}/ads/{adId}
Per-ad serve switch on Ads: pause or resume one ad across every platform it serves on. On live or paused campaigns, Autopilot manages serving ads: include reasonCode (and optional note) so Autopilot can yield; without it returns 400 override_required. Resume with resume_optimizer_pause for Autopilot pauses and resume_user_pause for manual pauses (mismatched dedicated codes return 400 resume_reason_mismatch; other remains an escape hatch). Refuses to pause the last serving ad on this interest segment line (409 last_serving_ad) and refuses on archived campaigns (409 campaign_archived).
body: { "serve": true|false, "reasonCode"?: "poor_quality"|"off_message"|"resume_optimizer_pause"|"resume_user_pause"|"other"|..., "note"?: "..." }{ "updated": true, "serving": false }GET/api/v1/tracking
Tracking setup for one website: the site tag snippet, server-side conversion webhook, conversion rules, and settings. Tags are per business: pass ?businessId= to pick a site (defaults to the oldest; other businesses are listed in the response).
{ "tracking": { "business", "snippet", "conversionWebhook": { "url", "fields", "example" }, "rules": [...], "settings": {...} }, "otherBusinesses": [] }POST/api/v1/tracking/rules
Create a conversion rule for a business. Intent-page URL patterns require acknowledgeIntentPage: true.
body: { "businessId", "name", "pattern", "valueCents"?, "acknowledgeIntentPage"? }201 { "rule": { "id", "name", "pattern", ... } }PATCH/api/v1/tracking/rules/{id}
Update a conversion rule's per-conversion value (cents).
body: { "valueCents": number|null }{ "rule": { "id", "valueCents", ... } }DELETE/api/v1/tracking/rules/{id}
Delete a conversion rule.
{ "ok": true }PATCH/api/v1/tracking/settings
Update tracking settings for a business (allowed origins, lead detection, etc.). Disabling recommended settings requires acknowledgeImpact: true.
body: { "businessId", "settings": {...}, "acknowledgeImpact"? }{ "settings": {...} }PATCH/api/v1/tracking/form-values
Set or clear the per-conversion value estimate for a named Form conversion (lead, signup, or subscribe). Creates a value-only carrier when needed; clearing with null removes the estimate. Applies to new conversions only.
body: { "businessId", "name": "lead"|"signup"|"subscribe", "valueCents": number|null }{ "ok": true, "businessId", "name", "valueCents" }POST/api/v1/tracking/suggestions/{id}
Accept or dismiss a conversion discovery suggestion. `{"action":"accept"}` creates or links a rule; `{"action":"dismiss"}` clears the card.
body: { "action": "accept"|"dismiss", "acknowledgeIntentPage"? }{ "ok": true, "status": "accepted"|"dismissed" }POST/api/v1/tracking/test
Send a test conversion event for a business (verifies the tag/webhook path).
body: { "businessId" }{ "ok": true }GET/api/v1/audiences
First-party segments with live member counts (Website audiences), platform audiences (campaign reach, synced lists, lookalikes), connected customer sources, plus Customize targeting guides (interest and demographic catalog picks Autopilot always includes on the next launch). Persona Reach lives on each persona via get_business (`targetingSeeds`).
{ "segments": [...], "platformAudiences": [...], "customerSources": [...], "marketplaceSegments": [{ "businessId", "segmentKey", "name", ... }], "demographicFilters": [{ "businessId", "segmentKey", "name", "kind", "eligible", ... }] }POST/api/v1/audiences/segments
Create a custom first-party segment with a rule definition.
body: { "businessId", "name", "rule": {...} }201 { "segment": { "id", "name", ... } }PATCH/api/v1/audiences/segments/{id}
Activate or pause a segment (`active`: true|false).
body: { "active": true|false }{ "segment": { "id", "active", ... } }DELETE/api/v1/audiences/segments/{id}
Delete a custom (non-system) segment.
{ "ok": true }POST/api/v1/audiences/segments/{id}/refresh
Enqueue a re-evaluation of segment membership.
{ "ok": true, "enqueued": true }POST/api/v1/audiences/customize
Add or remove a Customize targeting guide. kind: "interest"|"demographic"; action: "add"|"remove".
body: { "businessId", "kind": "interest"|"demographic", "action": "add"|"remove", "segmentKey" }{ "ok": true }POST/api/v1/audiences/suggestions/{id}
Accept or dismiss an audience discovery suggestion. Accept creates a discovered segment with the proposed rule.
body: { "action": "accept"|"dismiss" }{ "ok": true, "status": "accepted"|"dismissed" }PATCH/api/v1/audiences/consent
Set audience sync consent for the organization (required before syncing customer lists to ad platforms).
body: { "enabled": true|false }{ "ok": true, "audienceSyncEnabled": true }POST/api/v1/audiences/csv
Import a customer CSV for a business (seeds Audience boost). Rate limited. Hashed at rest; raw emails are never returned.
body: { "businessId", "csvText": "email,phone\n..." }202 { "ok": true, "imported": number, "skipped": number }GET/api/v1/integrations?businessId={id}
List connected customer-data sources for the organization (optional businessId filter). Tokens are never returned. Use POST .../sync to re-pull.
{ "integrations": [{ "id", "businessId", "provider", "status", "lastSyncedAt", "error" }] }POST/api/v1/integrations/{id}/sync
Enqueue a sync for an already-connected integration. OAuth connect/disconnect stay in the web app. 409 when the source is disconnected.
202 { "enqueued": true }POST/api/v1/personas
Create a persona from free-text notes for a business (enqueues enrichment).
body: { "businessId", "notes": "..." }202 { "personaId", "enqueued": true }PATCH/api/v1/personas/{id}
Partial update of a persona: name, description, demographics, motivations, pain points, channels (optional, machine-managed), selected flag, and/or demographicPins / demographicExclusions (catalog segment keys; pins capped at 3, must be eligible demographic filters).
body: { "name"?, "description"?, "selected"?, "demographicPins"?: string[], "demographicExclusions"?: string[], ... }{ "persona": { "id", "name", "selected", "demographicPins", "demographicExclusions", ... } }GET/api/v1/wallet
Prepaid wallet balance (cents) and auto-refill state. Check before launching; top-ups happen in the app.
{ "wallet": { "balanceCents", "autoRefillEnabled" } }GET/api/v1/wallet/ledger?cursor={c}&limit={n}
Paginated wallet ledger (newest first). limit default 25, max 100.
{ "entries": [{ "id", "kind", "amountCents", "createdAt", ... }], "nextCursor" }PATCH/api/v1/organization
Update organization product settings. crossCampaignOptimization enables automatic portfolio budget moves.
body: { "crossCampaignOptimization": true|false }{ "organization": { "crossCampaignOptimization" } }GET/api/v1/export
Full organization data export as one JSON document (rate limited to 5/hour). Large payload; prefer specialized endpoints when you only need one resource.
{ "businesses": [...], "campaigns": [...], "ledgerEntries": [...], "creativeFeedback": [...], ... }GET/api/v1/catalogs?businessId={id}
List Catalog Video product catalogs (optional businessId). Requires Catalog Video early access; otherwise returns 403.
{ "catalogs": [{ "id", "businessId", "name", "source", "status", "itemCount", ... }] }POST/api/v1/catalogs
Create a product catalog. source: "scrape"/"url" enqueue a sync; products appear once ingestion finishes (status starts "pending"). source: "csv" is parsed and inserted synchronously; status is "ready" immediately. Requires Catalog Video early access.
body: { "businessId", "source": "scrape|url", "sourceRef": "https://shop.example", "name"? }
or
{ "businessId", "source": "csv", "csvText": "sku,title,price,link,image_url\n...", "name"? }201 { "catalog": { "id", "status": "pending"|"ready", "itemCount" }, "importSummary"?: { ... } }GET/api/v1/catalogs/{id}/items?cursor={c}&limit={n}
List synced catalog products (SKU, title, price, image). Paginated: limit default 100, max 500; pass the response's nextCursor to fetch the next page.
{ "catalogId", "items": [{ "externalId", "title", "price", ... }], "nextCursor" }POST/api/v1/catalogs/{id}/items
Enqueue a catalog re-sync from its source.
{ "catalogId", "status": "syncing", "enqueued": true }POST/api/v1/catalog-video/from-url
Start the shop-URL → Catalog Video auto-pipeline for a business. Idempotent on (businessId, url). Requires Catalog Video early access.
body: { "businessId", "url", "topN"?: 1..10 }202 { "runId"?, "catalogId"?, ... }GET/api/v1/catalog-video/runs?businessId={id}&cursor={c}&limit={n}
List Catalog Video runs (optional businessId). Paginated: limit default 50, max 200; pass the response's nextCursor to fetch the next page.
{ "runs": [{ "id", "status", "completedCount", "totalCount", "error", "lastProgressAt", ... }], "nextCursor" }POST/api/v1/catalog-video/runs
Create a draft Catalog Video run linked to a catalog (optional template + selectedSkus). Requires Catalog Video early access.
body: { "catalogId", "templateId"?, "name"?, "selectedSkus"?, "aspectRatios"?, "defaultTier"?: "S|A|M|L|SWAP" }201 { "run": { "id", "status": "draft" } }GET/api/v1/catalog-video/runs/{id}
Catalog Video run detail with progress counters and status.
{ "run": { "id", "status", "completedCount", "totalCount", ... } }PATCH/api/v1/catalog-video/runs/{id}
Edit SKUs, aspect ratios, tier, sync cadence, refresh budget, new-item policy, or launch mode while draft/quoting/paused. Invalidates a prior quote.
body: { "selectedSkus"?, "aspectRatios"?, "defaultTier"?, ... }{ "run": { "id", "status", ... } }GET/api/v1/catalog-video/runs/{id}/quote?sample={true|false}&tranche={true|false}
Price a run from catalog SKUs at the default tier (S). sample=true quotes ~5 SKUs; tranche=true quotes the first chargeable chunk. Same planner as launch.
{ "quote": { "totalCents", "units": [...] }, "billing", "allowanceCents" }POST/api/v1/catalog-video/runs/{id}/launch
Charges the first tranche (creation credit then wallet), inserts pending variants, sets status producing, enqueues production. Pass the quote's mode + quoteHash to guard against launching a stale quote (409 quote_mismatch on drift); omitting both auto-quotes once for mode=full. 402 when funds are short. Requires an approved template when the run is linked to one.
body: { "mode"?: "sample|full", "quoteHash"? }{ "launched": true, "status": "producing", "mode", "chargedUnits", "totalUnits" }POST/api/v1/catalog-video/runs/{id}/pause
Manually pause a producing/live Catalog Video run.
body: { "reason"? }{ "run": { "id", "status": "paused" } }POST/api/v1/catalog-video/runs/{id}/resume
Resume a paused Catalog Video run and re-arm remaining production work.
{ "run": { "id", "status": "producing"|"live" } }POST/api/v1/catalog-video/runs/{id}/archive
Archive a draft/failed/paused/live Catalog Video run. Terminal, non-refunding.
{ "run": { "id", "status": "archived" } }POST/api/v1/catalog-video/runs/{id}/retry
Clear the error on a failed Catalog Video run and resume it.
{ "run": { "id", "status": "producing" } }GET/api/v1/catalog-video/runs/{id}/manifest?cursor={c}&limit={n}
Per-variant CDN / VAST URLs from the local variant rows. Paginated: limit default 100, max 500; pass the response's nextCursor to fetch the next page.
{ "runId", "variants": [{ "sku", "status", "latestCdnUrl", ... }], "nextCursor" }GET/api/v1/catalog-video/runs/{id}/feeds
Platform supplemental feed URLs once published on the run.
{ "runId", "feeds": [{ "platform", "url" }] }GET/api/v1/catalog-video/runs/{id}/exports?format={json|google_video_link}
Export activation kit: JSON with Google video_link instructions + CSV rows, or raw CSV (format=google_video_link). Operator handoff: Shopping/PMax not automated.
{ "runId", "googleVideoLink": { "instructions", "supplementalFeedUrl", "csvRows" } }GET/api/v1/catalog-video/templates?businessId={id}
List Catalog Video templates (optional businessId). Requires Catalog Video early access.
{ "templates": [{ "id", "businessId", "catalogId", "status", "aspectRatio", "hookCount", ... }] }GET/api/v1/catalog-video/templates/{id}
Full detail for one template, including the pitched concept / probe entry.
{ "template": { "id", "status", "hookCount", "entry", "masterCompositionId", ... } }POST/api/v1/catalog-video/templates/pitch
Create a pitched template and enqueue the template pipeline (human-review start).
body: { "catalogId", "aspectRatio"?, "hookCount"?, "notes"? }201 { "template": { "id", "status": "pitched" } }POST/api/v1/catalog-video/templates/{id}/approve
Human approve a template: required before launching a run that uses it.
{ "template": { "id", "status": "approved" } }GET/api/v1/campaigns/{id}/catalog-video
List bindable Catalog Video runs for a campaign and its current binding. Requires Catalog Video early access.
{ "currentRunId", "runs": [...] }POST/api/v1/campaigns/{id}/catalog-video
Bind or clear a campaign's Catalog Video run for native serving (runId null clears). Requires Catalog Video early access.
body: { "runId": "uuid"|null }{ "ok": true, "catalogVideoRunId": "uuid"|null }Billing model
Launched campaigns charge their daily budget from your prepaid wallet each day (US Eastern); pausing or ending stops future charges, and under-delivery reported by the ad platforms is credited back automatically. Ad creation is metered per unit: batches draw creation credit first (seeded at signup and earned back as a share of media spend), then the wallet. Price a batch up front with GET /api/v1/creatives/quote, and expect 402 with the quote attached when neither covers it. Failed renders refund their units automatically.
Catalog Video endpoints require early access for your organization. When enabled, launch charges the first production tranche the same way as other creation. Quote with /catalog-video/runs/{id}/quote first.