Create standalone ad
Creates 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
goalmay be omitted too (taken from the Campaign's objective), and the created ad'stargetingechoes 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,existingCampaignIdorbuyingType: "RESERVED"):budgetAmountandbudgetType, plusendDatefor a lifetime budget. - Single image or video (the default), also with
adSetId:headline(max 255),body,callToAction, exactly one ofimageUrl/video(none for a catalog template creative, which ignores and does not upload any sent), andlinkUrlunless the goal islead_generationorpage_visitsorleadGenFormIdis set. bodies[]/headlines[]text variations:body, one list with 2+ items,callToAction,imageUrlorvideo, andlinkUrlunless the goal islead_generationorleadGenFormIdis set.headlineis not required.placementAssets:headline,body,callToAction, andlinkUrlwith the same exemptions as the single shape; the media comes fromplacementAssets.carouselCards:body,callToAction, andlinkUrlunless the goal islead_generationorleadGenFormIdis set.headlineis not required.creatives[]: per entryheadline,body,callToAction,imageUrlorvideo, andlinkUrlunless the goal islead_generation. Top-levelbodyis not required.dynamicCreative:imageUrlsorvideoUrls,callToActionTypes, andlinkUrlsunless the goal islead_generation. Top-levelbodyis not required.existingCreativeId: no creative fields. Goalpage_likes:bodyonly.- Goal
lead_generationalways needsleadGenFormId, except withexistingCreativeId.
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.
Authorization
bearerAuth API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.
In: header
Header Parameters
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.
length <= 255Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
import Zernio from '@zernio/node';const zernio = new Zernio({ apiKey: process.env.ZERNIO_API_KEY });const { data } = await zernio.adcampaigns.createStandaloneAd({ body: { accountId: 'account_abc123', adAccountId: 'adaccount_abc123', name: 'Example', },});console.log(data);{ "validateOnly": true, "results": [ { "node": "campaign", "status": "validated", "reason": "string" } ], "message": "string"}Get ad details
Shows one ad with its creative, targeting, status and performance metrics. Google Search ads include current creative.headlines, creative.descriptions and creative.finalUrls, preserving pinnedField. Top-level cachedAt and stale report cache freshness. Google mutations invalidate this read. RSA enrichment requires a stored advertisingChannelType of SEARCH. Ads with an unknown or other channel return their stored details without a Google read. If RSA enrichment fails, the stored ad is returned with HTTP 200 and without cache metadata. The `{adId}` path segment accepts any identifier dialect Zernio indexes for the ad: - the Zernio internal `_id` (24-char hex) - Meta's numeric `platformAdId` (the value shipped in `comment.received` webhooks as `comment.ad.id`) - the creative's `effective_object_story_id` (`{pageId}_{postId}` shape, Facebook side) - the creative's `effective_instagram_media_id` (Instagram side) - an Instagram media id an ad comment arrived on (`comment.platformPostId` of a `comment.received` carrying `comment.ad.id`), including the extra media Meta creates for placement variants Any of these resolve to the same ad. Caller doesn't need a translation step. `creative.creativeFeatures` holds the stored requested settings, which do not confirm platform application. **Facebook ad dark posts.** Meta gives an ad with per-placement assets one extra unpublished post per placement, and comments land on it. Meta exposes no link from that post to its ad. When exactly one ad on the Page, created before the dark post, carries the same copy as the post, the dark post id resolves to that ad. Otherwise, when the id is a dark post Zernio has seen comments on and its Page is connected, the response is `ad: null` plus `darkPost`, which carries the Meta Ads account and the ad accounts running ads for that Page. Read its comments with GET /v1/ads/{adId}/comments using the same id. **Status freshness.** `status`, `configuredStatus`, `platformStatus`, `platformAdSetStatus` and `platformCampaignStatus` are the values Zernio last stored. Background sync refreshes them, typically within 15 to 60 minutes (Google up to about 3 hours), and ended or long-paused objects may be refreshed less often. Zernio's own status writes re-read the switches they change. A change made in the platform's own ads manager therefore shows up here only after the next sync. Controllers that act on a switch should pass `live=true`, which reads the switches from the platform now, stores them, and returns `statusReadAt` (null when the read failed and the stored values were returned). With `live=true` the ad's own switch (`configuredStatus`), its delivery status (`status`, `platformStatus`) and its ad set and campaign switches (`platformAdSetStatus`, `platformCampaignStatus`) are read live.
Boost post as ad
Turns an already published post into a paid ad that keeps the post's likes and comments, creating the campaign around it. By default it provisions the whole hierarchy (campaign, ad set, ad). **Attach shape (Meta).** Send `adSetId` to put the ad under an EXISTING ad set instead, so that ad set keeps its learning phase. It then owns `budget`, `schedule` and `targeting`, and sending any of those alongside `adSetId` is a 400 rather than a silent drop. `budget` is required only without `adSetId`. `instagramAccountId`, `destinationType`, `whatsappPhoneNumber` and `adSetId` are Meta-only and return 400 on other platforms. `accountId` may be a Facebook, Instagram or Meta ads (business login) connection. A business-login connection has no posting account, so pass the post as `platformPostId` (Facebook `pageId_postId` or an Instagram media id); a Zernio `postId` is a 400 there. For X, `accountId` may be the posting X account or its X Ads connection: either resolves the other on the same profile. A Zernio `postId` on an X Ads connection resolves through its X account, and is a 400 when the profile has none (pass `platformPostId` instead). **Reddit.** `accountId` is the Reddit Ads connection (platform `redditads`). Pass the Reddit post as `platformPostId` (`t3_abc123` or the bare id), or a Zernio `postId`, which resolves through the Reddit account connected on the same profile (400 when there is none). Reddit promotes only posts made by a profile of the ad account: a post by any other author returns 422. Goals are traffic, awareness, conversions, video_views (the post must be a video, 422 otherwise) and lead_generation; conversions and lead_generation need a pixel that already received the event (see `promotedObject`). `leadGenFormId` returns 400: Reddit retired its native lead forms, so `lead_generation` sends people to `linkUrl` and optimizes toward the pixel's Lead event. Each boost creates a campaign, an ad group carrying the budget, the targeting and the bid, and the ad. `bidStrategy` sets the ad group's bid: LOWEST_COST_WITHOUT_CAP (or omitted) is automatic bidding (BIDLESS), LOWEST_COST_WITH_BID_CAP is MANUAL_BIDDING with `bidAmount` as the bid, COST_CAP is MAXIMIZE_VOLUME with `bidAmount` as the cap; any other strategy returns 422 before anything is created. **Messaging boosts (Meta).** Use `goal: engagement` with `callToAction: WHATSAPP_MESSAGE`, `MESSAGE_PAGE`, or `INSTAGRAM_MESSAGE`. The CTA implies WHATSAPP, MESSENGER, or INSTAGRAM_DIRECT respectively; `destinationType` alone does not select a messaging CTA. Omit `linkUrl` only for messaging CTAs. Plain link CTAs keep their goal and link behavior when combined with an independent `destinationType`. The campaign uses OUTCOME_ENGAGEMENT and the ad set uses CONVERSATIONS with the promoted Page. Optional `whatsappPhoneNumber` selects a number already paired with that Page. Conflicting CTA/destination, instant form, goal, or optimizationGoal inputs return 400. Attach requires the target ad set destination to match. Existing post references preserve social proof; an Instagram reel rejected by Meta is not re-uploaded as a new post for a messaging boost. **Retries.** Boosts are NOT idempotent and can take minutes when Meta requires re-hosting an Instagram video, so do not retry on client timeout. Send an Idempotency-Key header to make retries safe: same key and body replays the original 201, and distinct keys always create distinct ads. Without the header, an identical request is treated as a retry: while one is in flight it returns 409, and within 10 minutes of a completed boost it returns the already-created ad instead of creating another. To intentionally duplicate an ad, send distinct Idempotency-Keys (or vary the body, e.g. the name).