Create standalone ad
Changed onCreates a paid ad with your own creative, building the campaign and ad set it needs or adding it to existing ones.
Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, OpenAI Ads (ChatGPT Ads) and Reddit Ads.
Google Performance Max: set campaignType: "pmax" and supply assetGroup with text, images by role, business name and finalUrl. Creates a daily budget, PAUSED campaign and asset group atomically. validateOnly: true validates the complete request with Google without creating or persisting resources. Read assets with GET /v1/ads/campaigns/{campaignId}/asset-groups. The logo is required; video is optional via assetGroup.youtubeVideoId. Brand guidelines are disabled at creation. All supplied asset links are validated together against Google's minimum asset requirements. PMax rejects ACTIVE creation, portfolio bidding, bid caps, legacy creative fields and attach shapes. Geo and language targeting are supported; omitted geo targets all locations. PMax does not require top-level goal, headline, body or linkUrl. Supported bidding: omitted or LOWEST_COST_WITHOUT_CAP for Maximize Conversions, COST_CAP plus bidAmount for target CPA, LOWEST_COST_WITH_MIN_ROAS plus roasAverageFloor for Maximize Conversion Value with target ROAS.
Google Demand Gen: set campaignType: "demand_gen" and supply demandGen. Creates a daily budget, PAUSED campaign, one ad group and one ad in a single atomic request: a multi-asset image ad, a video responsive ad when demandGen.youtubeVideoIds is sent, or a carousel ad (2 to 10 cards) when demandGen.carouselCards is sent. Geo (countries, regions, cities, zips, metros) and languages go on the ad group, as Demand Gen requires. demandGen.channels sets the ad group's channel controls and demandGen.audience builds a Google Audience (user lists, interests, custom audiences, age ranges, genders) attached to the ad group, or demandGen.audienceId attaches an existing one. validateOnly: true validates everything with Google without creating resources. Supported bidding: omitted or LOWEST_COST_WITHOUT_CAP for Maximize Conversions, COST_CAP plus bidAmount for target CPA, LOWEST_COST_WITH_MIN_ROAS plus roasAverageFloor for target ROAS, LOWEST_COST_WITH_BID_CAP plus bidAmount for target CPC. The created ad carries the native platformCampaignId, platformAdSetId (ad group) and platformAdId. Activate with PUT /v1/ads/campaigns/{campaignId}/status; budget, bidding and name are edited with the regular campaign endpoints; creative, channels, audience and ad group targeting with PUT /v1/ads/{adId} (demandGen, targeting). To grow an existing Demand Gen campaign, send existingCampaignId (adds a PAUSED ad group, with its own geo, languages, channels and audience, plus its ad) or adSetId (adds a PAUSED ad to that ad group); neither takes budget, bidding or schedule fields, and both support validateOnly.
Other mutually-exclusive request shapes are selected by the body:
- Legacy single-creative shape (all platforms, the default).
- Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting.
- Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted. On LinkedIn goal may be omitted too (taken from the Campaign's objective), and the created ad's targeting echoes the Campaign's audience.
Meta accepts creativeFeatures on the single and attach shapes and as defaults for creatives[]; an item replaces the whole feature map. promotion is not supported on any shape and any object is rejected with 400. Reusing existingCreativeId uses the existing creative settings instead of new settings. Requested settings are persisted for lists, exports, and default ad-detail reads.
Per-platform required fields, budget minimums, and video-ad rules are documented on each property below.
Meta required fields per shape (a missing one is a 400 naming it in param):
- Every shape that builds a new ad set (no adSetId, existingCampaignId or buyingType: "RESERVED"): budgetAmount and budgetType, plus endDate for a lifetime budget.
- Single image or video (the default), also with adSetId: headline (max 255), body, callToAction, exactly one of imageUrl / video (none for a catalog template creative, which ignores and does not upload any sent), and linkUrl unless the goal is lead_generation or page_visits or leadGenFormId is set.
- bodies[] / headlines[] text variations: body, one list with 2+ items, callToAction, imageUrl or video, and linkUrl unless the goal is lead_generation or leadGenFormId is set. headline is not required.
- placementAssets: headline, body, callToAction, and linkUrl with the same exemptions as the single shape; the media comes from placementAssets.
- carouselCards: body, callToAction, and linkUrl unless the goal is lead_generation or leadGenFormId is set. headline is not required.
- creatives[]: per entry headline, body, callToAction, imageUrl or video, and linkUrl unless the goal is lead_generation. Top-level body is not required.
- dynamicCreative: imageUrls or videoUrls, callToActionTypes, and linkUrls unless the goal is lead_generation. Top-level body is not required.
- existingCreativeId: no creative fields. Goal page_likes: body only.
- Goal lead_generation always needs leadGenFormId, except with existingCreativeId.
LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content "dark post" authored by a Company Page (see organizationId). Supported goals are engagement, traffic, awareness, and video_views (video ads use the video field; video_views requires a video), and traffic ads require linkUrl.
Reddit Ads (accountId of a redditads connection, adAccountId like a2_abc123) creates a campaign, an ad group carrying the budget, schedule, targeting and pixel, and an ad running a new promoted post authored by the ad account's first profile (422 when it has none). The creative format follows the body: carouselCards builds a carousel, video a video ad, imageUrl an image ad, and none of them a text ad, which cannot link out (linkUrl is a 400 there; body is required). Only the legacy single-creative shape is available: creatives[], adSetId, existingCampaignId and validateOnly are refused. Reddit throttling surfaces as 429 rate_limited with platform: reddit; retry after a delay. To promote an existing Reddit post use POST /v1/ads/boost.
Idempotency: this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an Idempotency-Key header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with Idempotent-Replayed: true) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). If the first request is cut off mid-create (for example by a function timeout), its key can stay in progress and keep returning 409 until it expires; use a new key to retry sooner, after checking whether the ad was created. Keys are scoped to your credential and expire after 24h.
Request
- Base URL: https://zernio.com/api
- URL: https://zernio.com/api/v1/ads/create
- Auth: HTTP bearer
Headers
Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409. A request cut off mid-handler (for example by a function timeout) can leave its key in progress, answering 409 until the key expires after 24 hours.
Request body
Example request
{
"creativeFeatures": {
"auto_promotion_tag": "OPT_IN"
},
"creatives": [
{
"creativeFeatures": {
"auto_promotion_tag": "OPT_IN"
}
}
],
"placementAssets": {
"defaultImageUrl": "https://example.com/feed-4x5.jpg",
"rules": [
{
"imageUrl": "https://example.com/feed-4x5.jpg",
"headline": "Build with Zernio",
"body": "Schedule social posts from your app.",
"description": "One API for your social content.",
"placements": {
"publisherPlatforms": [
"facebook"
],
"facebookPositions": [
"feed"
]
}
},
{
"imageUrl": "https://example.com/story-9x16.jpg",
"headline": "Your next story",
"body": "Bring your social workflow to Zernio.",
"placements": {
"publisherPlatforms": [
"facebook"
],
"facebookPositions": [
"story",
"facebook_reels"
]
}
}
]
},
"locationTargetingType": "presence",
"assetGroup": {
"finalUrl": "https://zernio.com",
"headlines": [
"Schedule posts",
"One social API",
"Build with Zernio"
],
"longHeadline": "Schedule social content from your app with Zernio",
"descriptions": [
"Connect your social accounts.",
"Publish and manage social content through one API."
],
"businessName": "Zernio",
"images": {
"landscape": [
"https://example.com/landscape.png"
],
"square": [
"https://example.com/square.png"
],
"logo": [
"https://example.com/logo.png"
]
}
},
"additionalHeadlines": [
"Schedule Your Posts",
{
"text": "Build With Zernio",
"pinnedField": "HEADLINE_2"
}
],
"additionalDescriptions": [
"Build social publishing into your application.",
{
"text": "Connect social accounts with one API.",
"pinnedField": "DESCRIPTION_2"
}
]
}Response
validateOnly dry-run passed, nothing was created
Changes
- breaking
added
subschema #1subschema #2to the response bodyoneOflist for the response status - info
removed
subschema #1subschema #2from the response bodyoneOflist for the response status
- breaking
- breaking
added
subschema #1subschema #2to the response bodyoneOflist for the response status - info
removed
subschema #1subschema #2from the response bodyoneOflist for the response status
- breaking