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:headlineis ignored,bodybecomesad_text). - Google: status, budget, KEYWORD edits via
targeting.keywords/targeting.negativeKeywords, DEVICE bid adjustments viatargeting.devices, LOCATION edits viatargeting.locations(or the equivalent top-leveltargeting.countries/regions/cities/zips/metros), and LANGUAGE edits viatargeting.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 othertargetingfield returns 400: Google cannot mutate it post-create without recreating the campaign. Creative edits are dispatched on the ad'sadvertisingChannelType, and every supported field replaces a whole set; a field you omit is preserved.- Search: top-level
headlines,descriptionsandfinalUrls. 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) andfinalUrls, pluscreative.longHeadline,creative.businessName,creative.imageUrl(the landscape marketing image) andcreative.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 updatescreative.assetResourceNameto the new asset, and each replaced image updates its own role increative.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, andassetGroupreturns 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.targetingtakes locations, languages,locationTargetingTypeanddevices; 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.
- Search: top-level
- 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
targetingorcreativereturns 501 with codefeature_not_available. OpenAI Ads budget is the campaign's spend cap, daily or lifetime (seebudget.typebelow). - Reddit Ads: status, budget,
name(renames the ad) and targeting.budgetgoes 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).targetingREPLACES the ad group's whole targeting (geo, communities, conversation keywords, interests, audiences, languages, gender), so it applies to every ad in the ad group.creativereturns 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.
Use PATCH /v1/ads/keywords/{keywordId} to pause/enable one keyword, or
DELETE /v1/ads/keywords/{keywordId} to remove it.
Authorization
bearerAuth API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.
In: header
Path Parameters
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
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.updateAd({ path: { adId: 'ad_abc123', }, body: { headlines: [ { text: 'Hello, world!', }, ], descriptions: [ { text: 'Hello, world!', }, ], finalUrls: [ 'https://example.com', ], },});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"}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).
Duplicate an ad
Copies a single Meta ad, optionally into another ad set, created paused so you can review it before launch. Duplicates a single ad via Meta's native `POST /{ad-id}/copies`. The copy is created paused. `adSetId` retargets the copy into another ad set; omitted = the source's own ad set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered automatically (`syncAfter: false` to skip). Creative settings returned by Meta, including explicit promotion metadata and creativeFeatures, are preserved when the native copy requires a creative rebuild. Metadata Meta does not return cannot be recovered. When Meta refuses the native copy with its capability error (code 3), which happens for some creatives built by other tools, the ad is rebuilt instead: a new creative from the source's returned spec and a new ad in the target ad set, carrying the source name, status option, rename options and tracking specs.