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).
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
import Zernio from '@zernio/node';const zernio = new Zernio({ apiKey: process.env.ZERNIO_API_KEY });const { data } = await zernio.adcampaigns.boostPost({ body: { accountId: 'account_abc123', adAccountId: 'adaccount_abc123', name: 'Example', goal: 'engagement', },});console.log(data);{ "ad": { "_id": "string", "name": "string", "platform": "facebook", "profileId": "string", "status": "active", "configuredStatus": "string", "reviewStatus": "in_review", "adType": "boost", "creativeType": "carousel", "goal": "engagement", "isExternal": true, "budget": { "amount": 0, "type": "daily", "daily": 0, "lifetime": 0, "pacing": "string" }, "metrics": { "spend": 0, "impressions": 0, "reach": 0, "clicks": 0, "ctr": 0, "cpc": 0, "cpm": 0, "engagement": 0, "conversions": 0, "allConversions": 0, "costPerConversion": 0, "actions": { "property1": 0, "property2": 0 }, "actionValues": { "property1": 0, "property2": 0 }, "purchaseValue": 0, "roas": 0, "costPerAction": { "property1": 0, "property2": 0 }, "outboundClicks": 0, "outboundClicksCtr": 0, "inlineLinkClicks": 0, "inlineLinkClickCtr": 0, "uniqueClicks": 0, "uniqueCtr": 0, "videoPlayActions": 0, "video30SecWatchedActions": 0, "videoThruplayWatchedActions": 0, "videoP25WatchedActions": 0, "videoP50WatchedActions": 0, "videoP75WatchedActions": 0, "videoP95WatchedActions": 0, "videoP100WatchedActions": 0, "video2SecWatchedActions": 0, "video6SecWatchedActions": 0, "video6SecFocusedViews": 0, "videoAvgTimeWatchedActions": 0, "costPerThruplay": 0, "funnel": { "landingPageViews": 0, "contentViews": 0, "searches": 0, "wishlistAdds": 0, "cartAdds": 0, "checkoutsInitiated": 0, "paymentInfoAdds": 0, "purchases": 0, "leads": 0, "registrationsCompleted": 0, "appInstalls": 0, "messagingConversationsStarted": 0, "messagingFirstReplies": 0 }, "engagementBreakdown": { "postEngagement": 0, "pageEngagement": 0, "reactions": 0, "comments": 0, "shares": 0, "saves": 0, "pageLikes": 0, "videoViews": 0, "linkClicks": 0 }, "lastSyncedAt": "2019-08-24T14:15:22Z" }, "platformAdId": "string", "platformAdAccountId": "string", "platformCampaignId": "string", "platformAdSetId": "string", "campaignName": "string", "adSetName": "string", "platformObjective": "string", "optimizationGoal": "string", "costType": "string", "servingStatuses": [ "string" ], "platformAdAccountName": "string", "platformCreatedAt": "2019-08-24T14:15:22Z", "bidStrategy": "LOWEST_COST_WITHOUT_CAP", "bidAmount": 0, "roasAverageFloor": 0, "promotedObject": { "custom_event_type": "string", "pixel_id": "string", "page_id": "string", "application_id": "string", "product_set_id": "string" }, "creative": { "assetGroup": { "name": "string", "finalUrl": "http://example.com", "headlines": [ "string", "string", "string" ], "longHeadline": "string", "descriptions": [ "string", "string" ], "businessName": "string", "images": { "landscape": [ "http://example.com" ], "square": [ "http://example.com" ], "logo": [ "http://example.com" ] }, "youtubeVideoId": "string" }, "assetGroupResourceName": "string", "headlines": [ { "text": "string", "pinnedField": "HEADLINE_1" }, { "text": "string", "pinnedField": "HEADLINE_1" }, { "text": "string", "pinnedField": "HEADLINE_1" } ], "descriptions": [ { "text": "string", "pinnedField": "DESCRIPTION_1" }, { "text": "string", "pinnedField": "DESCRIPTION_1" } ], "finalUrls": [ "http://example.com" ], "thumbnailUrl": "string", "imageUrl": "string", "assetResourceName": "string", "assets": { "marketingImages": [ { "resourceName": "string" } ], "squareMarketingImages": [ { "resourceName": "string" } ] }, "videoId": "string", "videoUrl": "string", "creativeId": "string", "creativeFeatures": { "property1": "OPT_IN", "property2": "OPT_IN" }, "catalog": { "productSetId": "string", "format": "single_image", "formatTransformationSpec": [ { "format": "string", "data_source": [ "string" ] } ] }, "objectType": "string", "objectStoryId": "string", "effectiveObjectStoryId": "string", "pageId": "string", "effectiveInstagramMediaId": "string", "instagramUserId": "string", "instagramPermalinkUrl": "string", "mediaUrls": [ "string" ], "carouselCards": [ { "imageUrl": "string", "imageHash": "string", "headline": "string", "description": "string", "linkUrl": "string", "callToAction": "string", "videoId": "string" } ], "isServing": true, "adFormat": "string", "servingHoldReasons": [ "string" ], "body": "string", "googleHeadline": "string", "googleDescription": "string", "youtubeVideoIds": [ "string" ], "linkUrl": "string", "whatsappPhoneNumber": "string", "pinterestImageUrl": "string", "pinterestTitle": "string", "pinterestDescription": "string" }, "targeting": {}, "schedule": { "startDate": "2019-08-24T14:15:22Z", "endDate": "2019-08-24T14:15:22Z" }, "rejectionReason": "string", "createdAt": "2019-08-24T14:15:22Z", "updatedAt": "2019-08-24T14:15:22Z" }, "message": "string"}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 `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.
Update ad
Changes an ad's status, budget, targeting or creative and applies the change on the platform. Patch one or more fields on an ad. Status, budget, targeting, and creative changes are propagated to the platform. Per-platform support: - **Meta** (Facebook + Instagram): all fields supported. - **TikTok**: status, budget, `name` (renames the ad), targeting (via `/v2/adgroup/update/`), and creative (via `/v2/ad/update/` patch-style: `headline` is ignored, `body` becomes `ad_text`). - **Google**: status, budget, KEYWORD edits via `targeting.keywords` / `targeting.negativeKeywords`, DEVICE bid adjustments via `targeting.devices`, LOCATION edits via `targeting.locations` (or the equivalent top-level `targeting.countries` / `regions` / `cities` / `zips` / `metros`), and LANGUAGE edits via `targeting.languages`. Each list you send becomes the FULL new set of its kind (criteria not in the list are removed, except devices, which Google cannot remove and which are switched off with a bid modifier of 0 instead); a kind left out is untouched. Any other `targeting` field returns 400: Google cannot mutate it post-create without recreating the campaign. Creative edits are dispatched on the ad's `advertisingChannelType`, and every supported field replaces a whole set; a field you omit is preserved. - **Search**: top-level `headlines`, `descriptions` and `finalUrls`. Use 3-15 headlines (1-30 characters) and 2-4 descriptions (1-90 characters). Omit an asset to remove it; omit pinnedField on an included asset to unpin it. Updates do not pad or truncate text. The legacy creative fields remain unsupported. - **Display**: top-level `headlines` (1-5, no pinnedField, display ads have no pinned positions), `descriptions` (1-5) and `finalUrls`, plus `creative.longHeadline`, `creative.businessName`, `creative.imageUrl` (the landscape marketing image) and `creative.squareImageUrl`. Each image URL is uploaded as a new Google asset and the ad is pointed at it; Google assets are immutable, so the previous asset stays in the account's asset library. A new landscape image also updates `creative.assetResourceName` to the new asset, and each replaced image updates its own role in `creative.assets`. - **Performance Max**: top-level `assetGroup`, which swaps asset roles on the ad's asset group. The other creative fields return 422 for this channel, and `assetGroup` returns 422 on any other channel. - **Demand Gen**: top-level `demandGen` (see GoogleDemandGenUpdate): creative, channel and audience changes in one atomic Google request. The other creative fields return 422 for this channel. `targeting` takes locations, languages, `locationTargetingType` and `devices`; locations and languages are written to the ad's ad group, which Demand Gen requires (campaigns migrated from Discovery that still target on the campaign keep being written there), so they apply to every ad in that ad group. - **LinkedIn**: status, budget, targeting (countries or regions, excludedLocations (countries), the B2B facets, and audience segments; applied to the LinkedIn Campaign via PARTIAL_UPDATE, and REPLACES the campaign's entire targetingCriteria, not a merge), and creative (uploads new media, creates a replacement inline creative on the same campaign, pauses the old one). - **Pinterest / X / OpenAI Ads**: status + budget only. Sending `targeting` or `creative` returns 501 with code `feature_not_available`. OpenAI Ads budget is the campaign's spend cap, daily or lifetime (see `budget.type` below). - **Reddit Ads**: status, budget, `name` (renames the ad) and targeting. `budget` goes to the ad's ad group, or to its campaign when the ad group spends a campaign budget, and keeps its daily or lifetime type (422 to switch). `targeting` REPLACES the ad group's whole targeting (geo, communities, conversation keywords, interests, audiences, languages, gender), so it applies to every ad in the ad group. `creative` returns 501: a Reddit ad runs a post, so create a new ad with the new creative instead. **Google location and language replacement:** locations, languages and devices are campaign-level criteria on Google, so these edits apply to every ad group and ad in the ad's campaign. Send the complete list you want to keep. Zernio diffs it against the campaign's live criteria and sends the removes and the creates in ONE `googleAds:mutate`, so the campaign is never left with a half-applied set; criteria already in the list keep their criterion ID and history. Excluded (negative) locations are left untouched. An empty location list returns 400 (a Google campaign with no location criteria targets every country, which is never what "remove my locations" means, so omit the field instead). Send either `targeting.locations` or the top-level geo fields, not both: mixing them returns 400. **Google radius targeting:** `customLocations` is editable and is replaced the same way, but as its OWN set. Google models a place (LOCATION) and a point plus radius (PROXIMITY) as different criterion types, so the two are independent: sending `customLocations` replaces every radius and leaves the cities and countries alone, and sending places replaces those and leaves the radius alone. Send `customLocations: []` to drop radius targeting entirely. A circle you re-send unchanged keeps its criterion ID rather than being removed and recreated. **Google keyword replacement:** These edits affect the ad's entire ad group, including sibling ads. Positive (`targeting.keywords`) and negative (`targeting.negativeKeywords`) sets are independent: omit a field to leave that set unchanged, or send `[]` to remove every keyword of that kind. Zernio compares each supplied set with Google's live criteria by case-insensitive keyword text and match type. A matching criterion is left untouched, retaining its criterion ID, enabled/paused status, keyword-level bid overrides, labels, and criterion-associated history/statistics. Zernio does not reset its quality score; Google continues to calculate scores and statistics normally. Text comparison does not trim whitespace. A bare string or an object without `matchType` means `broad`, not the existing criterion's match type. For example, resending an existing `{ "text": "plumber", "matchType": "exact" }` preserves it; sending `"plumber"` instead removes that EXACT criterion and requests a BROAD one. Changing text or match type removes criteria no longer requested and creates any missing criteria. New criteria get new IDs and do not inherit removed criteria's bid overrides, labels, or history. Historical reporting for a removed criterion is not transferred to its replacement. To add keywords without replacing a set, use [POST /v1/ads/keywords](https://docs.zernio.com/ad-campaigns/add-ad-keywords). Use `PATCH /v1/ads/keywords/{keywordId}` to pause/enable one keyword, or `DELETE /v1/ads/keywords/{keywordId}` to remove it.