Zernio
APIFor you
Zernio
APIFor you
API Reference

Campaigns and Ads

Overview

Get campaign treeGETGet daily account metricsGET

Campaigns

List campaignsGETCreate a standalone campaignPOSTGet live campaign detailsGETUpdate a campaignPUTDuplicate a campaignPOSTDelete a campaignDELETEPause or resume a campaignPUTPause or resume many campaignsPOSTList campaign assetsGETAttach campaign assetsPOSTUpdate campaign assetsPUTRemove campaign assetsDELETERead a Google campaign's device, location, excluded location, and language targetingGETEdit a Google campaign's device, location, excluded location, or language targetingPUTRead a campaign's ad schedule (dayparting)GETReplace a campaign's ad schedule (dayparting)PUTGet campaign conversion goalsGETUpdate campaign conversion goalsPATCH

Asset Groups

List Performance Max asset groupsGETCreate a Performance Max asset groupPOSTGet a Performance Max asset groupGETUpdate a Performance Max asset groupPATCHRemove a Performance Max asset groupDELETELink or unlink asset group assetsPOSTReplace an asset group's listing-group treePUT

Ad Sets

List ad setsGETCreate a standalone ad groupPOSTGet live ad-set detailsGETUpdate an ad setPUTDuplicate an ad setPOSTDelete an ad setDELETEPause or resume a single ad setPUTList ad-group assetsGETAttach ad-group assetsPOSTUpdate ad-group assetsPUTRemove ad-group assetsDELETE

Ads

List adsGETGet ad detailsGETCreate standalone adPOSTBoost post as adPOSTUpdate adPUTDuplicate an adPOSTCancel an adDELETEPause or resume a single adPUT

Keywords

List Search keywordsGETAdd Search ad-group keywordsPOSTPause or enable a Search keywordPATCHRemove a Search keywordDELETEList campaign-level negative keywordsGETReplace campaign-level negative keywordsPUTList campaign negative listsGETReplace campaign negative listsPUT

Bidding

Read a campaign's current biddingGETList portfolio bid strategiesGETCreate portfolio bid strategyPOSTUpdate portfolio bid strategyPATCH

Recommendations

List Google Ads recommendationsGETApply Google Ads recommendationsPOSTDismiss Google Ads recommendationsPOST

Other

Create a shared budgetPOSTRead the platform's review verdict for an adGETReview status of each creative in a TikTok Smart+ adGETList shared budgetsGETReview status of every TikTok Smart+ ad of an advertiserGET
Dashboard
OverviewPlatformsAPI ReferenceResources
Campaigns and Ads

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. Use PATCH /v1/ads/keywords/{keywordId} to pause/enable one keyword, or DELETE /v1/ads/keywords/{keywordId} to remove it.


PlatformsMetaGoogleTikTokLinkedInPinterestX
PUT
/v1/ads/{adId}

Authorization

bearerAuth
AuthorizationBearer <token>

API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.

In: header

Path Parameters

adId*string

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"}
Was this page helpful?

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.

headlines?array<>

Google Search and Display only. Replaces the complete headline list. Search takes 3-15, Display 1-5 and rejects pinnedField; the count is checked once the ad's channel is known. No padding or truncation on update.

Items1 <= items <= 15
descriptions?array<>

Google Search and Display only. Replaces the complete description list. Search takes 2-4, Display 1-5 and rejects pinnedField. No padding or truncation on update.

Items1 <= items <= 5
finalUrls?array<>

Google Search and Display only. Replaces final URLs. Omitted lists stay unchanged. For Performance Max use assetGroup.finalUrl.

Items1 <= items
assetGroup?

Google Performance Max only. Replaces whole asset roles on the ad's asset group. Returns 422 on any other platform or channel.

Properties1 <= properties
demandGen?

Google Demand Gen only. Returns 422 on any other platform or channel.

Properties1 <= properties
status?string

Value in

  • "active"
  • "paused"
budget?
targeting?

Meta + TikTok (demographics/interests), Google (keyword and device bid adjustment edits only), and LinkedIn (locations, B2B facets, audiences). Pinterest / X return 501.

On Meta, TikTok and LinkedIn this is a partial update: every targeting setting you do not send is preserved (on Meta: Advantage+ audience, placements, custom audiences, exclusions, interests, languages; on TikTok: languages, interest keywords, devices, behaviours; on LinkedIn: skills, member traits, interface locales, and any facet set in Campaign Manager). A field you send replaces its platform counterpart, and an empty or null value (for example audienceInclude: [], industries: [] or gender: "all") clears only that field. Any location field (countries, regions, cities, zips, ...) replaces the whole included location set; an update with no location keeps the current locations.

creative?

Replace or patch the ad's creative. Meta, TikTok, and LinkedIn.

  • Meta: patch-style. Pass any subset: fields you omit are preserved from the live creative, including media (image_hash/video_id are reused, no re-upload) and url_tags. Sending the full set (headline, body, callToAction, linkUrl, imageUrl) rebuilds the creative from scratch instead. Partial patching reads the live object_story_spec, which Meta strips on SHARE / page-post / dark / asset_feed creatives. Those return 422 asking for the full set. A videoUrl/videoId on an image creative is a type change and also needs the full set. existingCreativeId repoints the ad at a creative from GET /v1/ads/creatives and ignores every other field. Meta creatives are immutable, so any change creates a new creative and repoints the ad; the old creative is retained on the ad account for historical reporting. creativeFeatures is Meta-only. Omitted settings are preserved from the live creative, including full rebuilds. A supplied creativeFeatures map overrides individual existing keys.
  • TikTok: patch-style. Pass any subset; headline is ignored (TikTok creatives have no headline slot). body becomes the in-feed ad_text; linkUrl becomes landing_page_url; videoUrl triggers a fresh upload. description, videoId and existingCreativeId are Meta-only and return 400.
  • LinkedIn: requires new media (image via imageUrl or video via videoUrl); a text-only creative update returns 400. Uploads the media, creates a new inline media creative on the same campaign, and pauses the old creative (best-effort). The old creative is retained for historical reporting. videoId and existingCreativeId are Meta-only and return 400.
name?string

Rename the ad. Now propagated to Meta (POST /{ad-id}); non-Meta platforms return 501.

Lengthlength <= 255