---
title: "Boost post as ad"
method: POST
path: "/v1/ads/boost"
tags: ["Ad Campaigns"]
---

# Boost post as ad

`POST /v1/ads/boost`

Creates a paid ad from an existing published post, keeping the post's
engagement. 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` and `adSetId` are Meta-only and
return 400 on other platforms.

## Request body

- object
  - `postId` string — Zernio post ID (provide this or platformPostId)
  - `platformPostId` string — Platform post ID (alternative to postId)
  - `accountId` string, required — Social account ID
  - `adAccountId` string, required — Platform ad account ID
  - `name` string, required
  - `goal` 'engagement' | 'traffic' | 'awareness' | 'video_views' | 'lead_generation' | 'conversions' | 'app_promotion', required — Available goals vary by platform. Meta (Facebook/Instagram) and TikTok support all 7. LinkedIn supports all except app_promotion. Twitter/X supports engagement, traffic, awareness, video_views, app_promotion. Pinterest and Google Ads support only engagement, traffic, awareness, video_views.
  - `adSetId` string — Meta only. Attach the boosted post to this existing ad set instead of creating a campaign. The ad set then owns budget, schedule and targeting; sending those too is a 400.
  - `budget` object — Required unless adSetId is set.
    - `amount` number, required — Minimum varies: TikTok=$20, Pinterest=$5, others=$1
    - `type` 'daily' | 'lifetime', required
  - `instagramAccountId` string — Meta only. Instagram identity the ad runs AS (creative.instagram_user_id), overriding the account linked to the Page. Live-verified against a Page-post creative.
  - `destinationType` 'INSTAGRAM_PROFILE' | 'WEBSITE' | 'ON_AD' | 'MESSENGER' | 'WHATSAPP' — Meta only. Ad-set destination_type — where the click LANDS, as opposed to instagramAccountId which is who the ad runs as. Lead ads force ON_AD and ignore this.
  - `currency` string
  - `schedule` object
    - `startDate` string, date-time
    - `endDate` string, date-time — Required for lifetime budgets
  - `targeting` object — Same geo/demographic fields as the `TargetingSpec` used by /v1/ads/create. Geo keys (`regions`/`cities`/`zips`/`metros`) resolve via GET /v1/ads/targeting/search?dimension=geo. City radius and lat/lng `customLocations` are Meta-only and preserve the boosted post's social proof (the ad references the existing post).
    - `ageMin` integer
    - `ageMax` integer
    - `gender` 'all' | 'male' | 'female' — Applied on Meta, TikTok and Pinterest. Ignored on Google, LinkedIn and X.
    - `languages` string[] — Meta locale ids (numeric), passed through as given.
    - `countries` string[] — ISO country codes. Required for TikTok boosts (TikTok's ad group requires location_ids); optional on other platforms.
    - `regions` object[] — Region/state targeting. `key` from /v1/ads/targeting/search?dimension=geo&geoType=region.
      - `key` string, required
      - `name` string
    - `cities` object[] — City targeting. Optional `radius` + `distanceUnit` extend beyond the city limits (both set together, Meta only).
      - `key` string, required
      - `name` string
      - `radius` number — Requires distanceUnit. Meta enforces a minimum city radius (~17 km / 10 mi); smaller values resolve to a 0-size audience and the ad fails at launch. For a tighter catchment use customLocations (lat/lng).
      - `distanceUnit` 'mile' | 'kilometer'
    - `zips` object[] — Postal/ZIP targeting. `key` is the platform's postal location ID (e.g. Meta `US:94304`).
      - `key` string, required
      - `name` string
    - `metros` object[] — DMA / metro-area targeting. `key` is the platform's metro ID (e.g. Meta `DMA:807`).
      - `key` string, required
      - `name` string
    - `customLocations` object[] — Point-radius (lat/lng) targeting (Meta custom_locations). No geo `key` lookup needed.
      - `latitude` number, required
      - `longitude` number, required
      - `radius` number, required
      - `distanceUnit` 'mile' | 'kilometer', required
      - `name` string
      - `address` string — Optional label, sent to Meta as `address_string`.
    - `interests` object[] — Interest objects from /v1/ads/interests. Each must include id and name.
      - `id` string, required
      - `name` string, required
    - `advantage_audience` 0 | 1 — Meta only. 0 = disabled (default), 1 = enabled.
  - `rawTargeting` object — Meta only. A Meta-native targeting spec (e.g. `{ "geo_locations": { "cities": [{ "key": "...", "radius": 15, "distance_unit": "kilometer" }] } }`). Sent alone it is forwarded unchanged. Use for advanced fields the structured object does not expose (flexible_spec, excluded audiences, business places, user_os, wireless_carrier). Can be combined with `targeting`: rawTargeting is the BASE layer and the built camelCase spec is merged on top, key by key (camelCase wins on collision). The merge goes one level deep inside `geo_locations` and `excluded_geo_locations` (built sub-keys win; raw-only sub-keys such as `location_types` survive). Array values (`flexible_spec`, ...) are replaced as a whole key, never element-merged. When `rawTargeting` is present the `advantage_audience: 0` default that Zernio normally applies is no longer emitted, so it cannot clobber a `targeting_automation` sent in the raw spec. Meta requires `targeting_automation` on ad set creation, so include it in the raw spec, or send `targeting.advantage_audience` (0 or 1), which is merged over raw as `targeting_automation`.
  - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS' — Meta bid strategy. Same enum applies at campaign and ad-set level; ad-set value (when set) overrides campaign-level. Cross-field rules: - `LOWEST_COST_WITHOUT_CAP` (default): auto-bid, forbids `bidAmount` and `roasAverageFloor`. - `LOWEST_COST_WITH_BID_CAP` / `COST_CAP`: require `bidAmount` (whole currency units). - `LOWEST_COST_WITH_MIN_ROAS`: requires `roasAverageFloor` (decimal multiplier, 2.0 = 2.0x). Source: facebook-business-sdk-codegen api_specs/specs/enum_types.json (`AdSet_bid_strategy`, `Campaign_bid_strategy`).
  - `bidAmount` number — Bid cap in WHOLE currency units (USD: 5 = $5.00; JPY: 100 = ¥100). Required when `bidStrategy` is `LOWEST_COST_WITH_BID_CAP` or `COST_CAP`. Backward-compat: providing `bidAmount` without `bidStrategy` is treated as `LOWEST_COST_WITH_BID_CAP`.
  - `roasAverageFloor` number — Minimum ROAS as a decimal multiplier (e.g. 2.0 = 2.0x ROAS). Required when `bidStrategy` is `LOWEST_COST_WITH_MIN_ROAS`. Sent to Meta as `bid_constraints.roas_average_floor` × 10000 (Meta uses fixed-point integers).
  - `platformSpecificData` LinkedInAdsPlatformData — LinkedIn-specific options for POST /v1/ads/boost and POST /v1/ads/create: campaign bidding and delivery controls, plus the LinkedIn-only creative formats on /v1/ads/create. Unknown keys are rejected.
    - `costType` 'CPM' | 'CPC' | 'CPV' — Campaign cost model (billing event). Defaults to `CPM`. Required when `unitCost` is set so the manual bid applies to an explicit cost model.
    - `unitCost` number — Manual bid in WHOLE account-currency units (e.g. 2.5 = $2.50). Requires `costType`. Omit for LinkedIn's automated (max delivery) bidding. LinkedIn enforces its own per-audience min/max bid bounds.
    - `optimizationTargetType` string — Campaign `optimizationTargetType` (e.g. `MAX_CLICK`, `TARGET_COST_PER_CLICK`, `MAX_IMPRESSION`). Forwarded verbatim, LinkedIn validates compatibility with the objective and `costType`. Omit for the objective-derived default: `awareness` gets `MAX_IMPRESSION`, `video_views` gets `MAX_VIDEO_VIEW`, and every other goal gets `MAX_CLICK`. `lead_generation` and `conversions` also get `MAX_CLICK`, because `MAX_LEAD` and `MAX_CONVERSION` need a lead gen form or a conversion rule that neither creation flow attaches. The default applies only to `SPONSORED_UPDATES` campaigns (every boost, and the image, video and carousel standalone ads), never to the `TEXT_AD`, `DYNAMIC` and `SPONSORED_INMAILS` campaigns the other creative formats produce. It is also skipped when `unitCost` or a non-`CPM` `costType` is set, since those select manual bidding and the bid is then yours to choose.
    - `creativeSelection` 'OPTIMIZED' | 'ROUND_ROBIN' — How LinkedIn rotates creatives within the campaign. Defaults to `OPTIMIZED`.
    - `audienceExpansionEnabled` boolean — Enable LinkedIn audience expansion. Defaults to false.
    - `offsiteDeliveryEnabled` boolean — Deliver on the LinkedIn Audience Network. Defaults to false.
    - `connectedTelevisionOnly` boolean — Restrict delivery to Connected TV inventory.
    - `carousel` object — POST /v1/ads/create only. Carousel ad with 2-10 image cards. Mutually exclusive with the other creative sources.
      - `cards` object[], required
        - `imageUrl` string, uri, required
        - `headline` string — Card title. Falls back to the ad-level headline.
        - `landingUrl` string, uri — Per-card click destination. LinkedIn requires one on every card; the ad-level `linkUrl` backfills cards that omit it.
    - `document` object — POST /v1/ads/create only. Document ad rendered as an in-feed viewer. PDF, PPT or DOC up to 100MB. Mutually exclusive with the other creative sources.
      - `url` string, uri, required
      - `title` string, required — Document title.
    - `spotlight` object — POST /v1/ads/create only. Dynamic Spotlight Ad personalized with the viewer's profile photo. Supported goals: traffic, awareness. logoUrl and organizationName default to the Company Page's; set them explicitly if LinkedIn rejects the create with a 404. Mutually exclusive with the other creative sources.
      - `headline` string, required
      - `description` string — Mutually exclusive with backgroundImageUrl.
      - `callToAction` string, required — Button label text.
      - `landingUrl` string, uri, required
      - `logoUrl` string, uri
      - `organizationName` string
      - `showMemberProfilePhoto` boolean — Defaults to true.
      - `backgroundImageUrl` string, uri — Custom background. Replaces the description and the profile photo.
    - `follower` object — POST /v1/ads/create only. Dynamic Follower Ad promoting the Company Page. Supported goals: engagement, awareness. headline and description take exactly one of preApproved or custom. Mutually exclusive with the other creative sources.
      - `headline` object, required
        - `preApproved` string — LinkedIn preset id, not reviewed. Example GROW_YOUR_BUSINESS_INSIGHTS.
        - `custom` string — Free text, reviewed by LinkedIn.
      - `description` object, required
        - `preApproved` string — LinkedIn preset id, not reviewed. Example GET_LATEST_JOBS_AND_INDUSTRY_NEWS.
        - `custom` string — Free text, reviewed by LinkedIn.
      - `callToAction` 'VISIT_ORGANIZATION_COMPANY_PAGE' | 'VISIT_ORGANIZATION_LIFE_PAGE' | 'VISIT_ORGANIZATION_JOBS_PAGE' | 'VISIT_ORGANIZATION_CAREERS_PAGE', required
      - `logoUrl` string, uri
      - `organizationName` string
      - `showMemberProfilePhoto` boolean — Defaults to true.
    - `jobs` object — POST /v1/ads/create only. Dynamic Jobs Ad promoting your open roles, personalized with the viewer's profile photo. Requires goal job_applicants and a Company Page with active job postings. headline and buttonLabel take exactly one of preApproved or custom. logoUrl and organizationName default to the Company Page's. Mutually exclusive with the other creative sources.
      - `headline` object, required
        - `preApproved` string — LinkedIn preset id, not reviewed. Example MEMBER_READY_FOR_YOUR_DREAM_JOB.
        - `custom` string — Free text, reviewed by LinkedIn.
      - `buttonLabel` object, required
        - `preApproved` string — LinkedIn preset id, not reviewed. One of SEE_MORE_JOBS, VIEW_MORE, CAREERS_AT_COMPANY.
        - `custom` string — Free text, reviewed by LinkedIn.
      - `logoUrl` string, uri
      - `organizationName` string
      - `showMemberProfilePhoto` boolean — Defaults to true.
    - `textAd` object — POST /v1/ads/create only. Classic right-rail Text Ad. The copy lives here; ad-level body and headline are not used. Mutually exclusive with the other creative sources.
      - `headline` string, required
      - `description` string, required
      - `landingUrl` string, uri, required
      - `imageUrl` string, uri — Optional 100x100 image.
    - `conversation` object — POST /v1/ads/create only. Conversation Ad: a choose-your-path message tree delivered to the member's LinkedIn inbox. Messages are flat nodes wired by local ids; each button either opens a url or leads to nextMessageId. Cycles, unknown ids and a missing firstMessageId return a 400. LinkedIn does not deliver message ads to EU members. Mutually exclusive with the other creative sources.
      - `subject` string, required — InMail subject shown in the inbox.
      - `sender` string — Person or organization URN. Defaults to the authoring Company Page. The sender must be approved for the ad account first (Campaign Manager > Manage message ad senders) or LinkedIn rejects the create with SINMAIL_SENDER_NOT_APPROVED.
      - `body` string — Optional intro body (HTML allowed).
      - `footer` string — Terms shown at the bottom of the message.
      - `headline` string — Conversation headline. Defaults to the first message's first line.
      - `firstMessageId` string, required
      - `messages` object[], required
        - `id` string, required
        - `text` string, required
        - `buttons` object[]
          - `text` string, required
          - `nextMessageId` string — Continues the conversation at this message. Exactly one of nextMessageId or url.
          - `url` string, uri — Opens this landing page. Exactly one of nextMessageId or url.
    - `event` object — POST /v1/ads/create only. Promotes an existing LinkedIn Event; no headline needed. Mutually exclusive with the other creative sources.
      - `urn` string, required — LinkedIn Event URN, urn:li:event:N.
    - `thoughtLeader` object — POST /v1/ads/create only. Sponsors an existing LinkedIn post (a share or ugcPost authored by your organization's Company Page) as the creative, keeping its commentary, author and engagement. Unlike boostPost, which provisions its own CampaignGroup + Campaign around the post, this variant attaches the reference under the campaign /v1/ads/create builds — same shape as every other format, so the caller can pick bidding / targeting / schedule freely. No headline, body, imageUrl or organization are needed; the referenced post carries its own commentary and author. Mutually exclusive with the other creative sources. Posts from personal profiles (Thought Leader Ads) are NOT supported (see postUrn).
      - `postUrn` string, required — LinkedIn share or ugcPost URN, urn:li:share:N or urn:li:ugcPost:N. Get it via "Copy link to post" on the target LinkedIn post (the URL contains -share- for a share or -ugcPost- for a ugcPost, then the numeric id). The post must be authored by an organization (Company Page). Member (personal profile) posts, i.e. Thought Leader Ads proper, are rejected by LinkedIn's public Marketing API regardless of sponsorship approval and of post type (a LinkedIn limitation; their Campaign Manager creates those through a private API). Referencing a member post returns a 422 with a clear error.
  - `tracking` object — Meta only. Tracking specs (pixel, URL tags).
    - `pixelId` string
    - `urlTags` object[] — URL parameters appended to the ad link, rendered as `key=value` pairs joined with `&`. Meta dynamic macros ({{ad.id}}, {{campaign.id}}, {{placement}}, ...) are sent through unescaped so Meta expands them; every other character is percent-encoded.
      - `key` string
      - `value` string
  - `specialAdCategories` string[] — Meta only. Required for housing, employment, credit, or political ads.
  - `specialAdCategoryCountry` string[] — Meta (metaads) only. 2-letter ISO country codes the special ad category applies to. Requires specialAdCategories to be set (400 otherwise).
  - `linkUrl` string, uri — Destination URL for the CTA button. Send it together with `callToAction`. **Meta**: adds a top-level `call_to_action` to the post-reference creative. This is what gives a `traffic` boost a clickable destination without replacing the creative and losing the post's social proof. Ignored when `leadGenFormId` is set, which supplies its own destination. Live-verified against a Page-post creative. **TikTok**: maps to `landing_page_url` on the Spark Ad creative (`AdcreateCreatives.landing_page_url`); Spark Ads have no clickable destination without it. Ignored on LinkedIn / Pinterest / X / Google, which infer the destination from the boosted post.
  - `callToAction` string — CTA button label. Send it together with `linkUrl` — a CTA without a destination produces a button that goes nowhere, so sending one alone is a 400. **Meta**: validated against the Meta CTA enum (same values as POST /v1/ads/create), e.g. `LEARN_MORE`, `SHOP_NOW`, `SIGN_UP`. **TikTok**: pass-through to `call_to_action` on the Spark Ad creative; the platform validates the value. See TikTok's "Enumeration - Call-to-Action".
  - `sparkAuthCode` string — TikTok-only. Spark Code (creator's `auth_code`) authorizing cross-creator Spark Ads — the advertiser can boost a video owned by a DIFFERENT TikTok account. Without this, boosts are limited to videos owned by the same account running the ads (same-BC creators only). The creator generates the code in their TikTok app's Promote settings and shares it with the advertiser. Maps to `auth_code` on the creative entry of /v2/ad/create/.
  - `dsaBeneficiary` string — Legal entity that benefits from the ad. Required when targeting EU users (EU DSA, Article 26). Optional if the ad account has a default beneficiary: set it once via `PATCH /v1/ads/accounts` or in Meta Ads Manager, and Meta fills it in whenever the field is omitted.
  - `dsaPayor` string — Legal entity that pays for the ad. Can differ from `dsaBeneficiary` (for example, an agency paying for a client's ads). Same rules as `dsaBeneficiary`: required for EU targeting unless the ad account has a default payor.
  - `optimizationGoal` string — Meta only. Explicit ad-set `optimization_goal` override. When omitted, defaults to the value derived from `goal`. The value must be compatible with the objective Meta derives from `goal`, not with the objective used by `POST /v1/ads/create` for the same `goal` name: boost maps `goal: "engagement"` to objective `OUTCOME_AWARENESS`, which accepts `REACH`, `IMPRESSIONS`, `AD_RECALL_LIFT`, or THRUPLAY-class values, and rejects `POST_ENGAGEMENT` (that value is only valid under `OUTCOME_ENGAGEMENT`, which create uses for the same goal name).

## Response `201`

Ad created

- object
  - `ad` Ad
    - `_id` string
    - `name` string
    - `platform` 'facebook' | 'instagram' | 'tiktok' | 'linkedin' | 'pinterest' | 'google' | 'twitter' | 'openai'
    - `status` 'active' | 'paused' | 'pending_review' | 'rejected' | 'completed' | 'cancelled' | 'error'
    - `configuredStatus` string, nullable — The ad's own on/off toggle as configured on the platform (Meta `configured_status`: ACTIVE / PAUSED), unaffected by ancestor (ad set / campaign) pauses. Distinct from `status`, which is the ancestor-cascaded delivery status. Only present for Meta ads synced after this field was added.
    - `reviewStatus` 'in_review' | 'approved' | 'rejected' | 'with_issues' — Platform-side review state, independent of the delivery `status` and the `configuredStatus` on/off toggle. `in_review` means the platform is still reviewing. Absent when the platform reports no review signal (e.g. a paused ad whose review state is masked behind the pause).
    - `adType` 'boost' | 'standalone'
    - `creativeType` 'carousel' | 'video' | 'document' | 'image' | 'null', nullable — Creative format, classified from the media the creative carries. `null` when the creative carries no media to classify — an unsynced creative and a genuine text-only ad are indistinguishable, so neither is guessed at. Returned by `GET /v1/ads`, `GET /v1/ads/{adId}` and the ad nodes of `GET /v1/ads/tree`.
    - `goal` 'engagement' | 'traffic' | 'awareness' | 'video_views' | 'lead_generation' | 'lead_conversion' | 'conversions' | 'app_promotion' | 'catalog_sales' | 'job_applicants' — Available goals vary by platform. Meta (Facebook/Instagram) supports all 9 (incl. `lead_conversion` = website pixel lead optimization and `catalog_sales` = Advantage+ catalog ads). TikTok supports the 7 non-`lead_conversion` goals. LinkedIn supports all except app_promotion / lead_conversion. Twitter/X supports engagement, traffic, awareness, video_views, app_promotion. Pinterest and Google Ads support only engagement, traffic, awareness, video_views.
    - `isExternal` boolean — True for ads synced from platform ad managers
    - `budget` object, nullable
      - `amount` number
      - `type` 'daily' | 'lifetime'
    - `metrics` AdMetrics
      - `spend` number
      - `impressions` integer
      - `reach` integer — Unique people reached in the requested date range. Meta (facebook/instagram) and TikTok: the platform's own de-duplicated reach for the exact range, fetched live and cached up to ~1 hour (may lag recent delivery; on a transient platform error the value temporarily falls back to a sum of per-day reach, which overcounts people reached on multiple days or by multiple child ads). Because it is de-duplicated, reach is NOT additive on these platforms: neither daily values nor child nodes sum to the range total. Google, LinkedIn, X, Pinterest and OpenAI report 0 (reach not synced). Frequency (impressions / reach) is only meaningful for Meta and TikTok.
      - `clicks` integer
      - `ctr` number — Click-through rate (%)
      - `cpc` number — Cost per click
      - `cpm` number — Cost per 1000 impressions
      - `engagement` integer
      - `conversions` number — Count of conversion events over the requested date range. FRACTIONAL: attribution splits one conversion across touchpoints and Google additionally reports modeled conversions, so values like 0.347 are normal. Meta: events matching the campaign's promoted_object.custom_event_type (PURCHASE, LEAD, etc.). Google: the account's tracked conversions. X and LinkedIn: their reported website/lead conversions (added 2026-07). 0 for non-conversion campaigns or when no events have fired.
      - `costPerConversion` number — Derived spend / conversions in the same currency as spend. 0 when conversions is 0.
      - `actions` object — Per-action-type counts summed over the date range, keyed by the platform's action-type names. Meta: raw Insights action_type keys (link_click, offsite_conversion.fb_pixel_purchase, onsite_conversion.lead_grouped, ...) — both engagement and conversion events. X: conversion types (purchase, sign_up, site_visit, download, custom). LinkedIn: conversion types (post_click, post_view, lead_gen). Google returns {} (its per-action names aren't synced per ad). Empty object when no actions are reported. NOTE: keys differ by platform, so branch on the ad's platform when interpreting them.
      - `actionValues` object — Monetary mirror of `actions`, from Meta's Insights `action_values[]` array. Same keying — values are the revenue attributed to each action_type, in ad-account native currency (same unit as `spend`; see the campaign node's `currency` field). Use this to compute revenue-per-event (e.g. avg purchase value). Meta-only; other platforms return {}.
      - `purchaseValue` number — Convenience sum of purchase-type action values — picked from `actionValues` via the same priority list as `conversions` so both fields describe the same events. In ad-account native currency. 0 when the campaign has no purchase event configured. Meta-only.
      - `roas` number — Return on ad spend — derived as `purchaseValue / spend`. 0 when `spend` is 0. Equivalent to Meta's `purchase_roas` under default attribution. At ad-set and campaign levels this is recomputed from summed purchaseValue + spend (NOT averaged across children) so it's mathematically correct at every rollup level.
      - `videoPlayActions` integer — Meta video ads only (0 for non-video ads and other platforms), like all video* fields below. Number of times the video started playing (Meta `video_play_actions`), summed over the date range and across children at ad-set/campaign level.
      - `video30SecWatchedActions` integer — Views of at least 30 seconds (or to the end, for shorter videos). Meta `video_30_sec_watched_actions`.
      - `videoThruplayWatchedActions` integer — ThruPlays (watched to completion, or at least 15 seconds). Meta `video_thruplay_watched_actions`.
      - `videoP25WatchedActions` integer — Views reaching 25% of the video's length. With the other percentile fields, powers hook/hold/drop-off analysis (e.g. hook rate = videoP25WatchedActions / videoPlayActions). Meta `video_p25_watched_actions`.
      - `videoP50WatchedActions` integer — Views reaching 50% of the video's length. Meta `video_p50_watched_actions`.
      - `videoP75WatchedActions` integer — Views reaching 75% of the video's length. Meta `video_p75_watched_actions`.
      - `videoP95WatchedActions` integer — Views reaching 95% of the video's length. Meta `video_p95_watched_actions`.
      - `videoP100WatchedActions` integer — Views reaching 100% of the video's length. Meta `video_p100_watched_actions`.
      - `videoAvgTimeWatchedActions` number — Average seconds watched per play (Meta `video_avg_time_watched_actions`). Aggregated over date ranges and across children as a play-weighted average (total watch time / total plays), never a plain average of averages.
      - `costPerThruplay` number — Derived `spend / videoThruplayWatchedActions`, in ad-account native currency. Rounded to 4 decimals rather than the usual 2 because a ThruPlay routinely costs well under a cent. 0 when the ad has no ThruPlays.
      - `funnel` AdFunnelCounts — Named conversion-funnel steps, resolved from the same data as `actions` so you never have to parse action-type strings yourself. Meta reports one event under several action types at once (`offsite_conversion.fb_pixel_purchase`, `omni_purchase`, `purchase`, …). Each field below takes the FIRST family member present rather than summing them, which is what makes these counts safe to add up — summing the raw `actions` keys yourself double or triple counts. The same priority order backs `conversions`, so a purchase-optimised campaign reports the identical number in `conversions` and `funnel.purchases`. Every field is 0 when that step never fired. Populated for Meta ads; other platforms report a different action taxonomy and generally leave these at 0 (read `actions` for those). At ad-set and campaign level each step is summed from its per-ad values.
        - `landingPageViews` integer — Landing page views — the visitor actually loaded the destination, unlike a link click. Meta `landing_page_view`.
        - `contentViews` integer — Content views (Meta `ViewContent` pixel event).
        - `searches` integer — On-site searches (Meta `Search` pixel event).
        - `wishlistAdds` integer — Adds to wishlist (Meta `AddToWishlist` pixel event).
        - `cartAdds` integer — Adds to cart (Meta `AddToCart` pixel event).
        - `checkoutsInitiated` integer — Checkouts started (Meta `InitiateCheckout` pixel event).
        - `paymentInfoAdds` integer — Payment details added at checkout (Meta `AddPaymentInfo` pixel event).
        - `purchases` integer — Purchases (Meta `Purchase` pixel event). Pair with `purchaseValue` for revenue.
        - `leads` integer — Leads, from either the website pixel or an instant form — whichever the ad uses.
        - `registrationsCompleted` integer — Completed registrations (Meta `CompleteRegistration` pixel event).
        - `appInstalls` integer — Mobile app installs attributed to the ad.
        - `messagingConversationsStarted` integer — Messaging conversations started within 7 days — the headline metric for click-to-WhatsApp and click-to-Messenger ads.
        - `messagingFirstReplies` integer — Messaging threads where the person sent a first reply.
      - `engagementBreakdown` AdEngagementCounts — The single `engagement` total split into the interactions behind it. Note that `engagement` is not the sum of these: Meta's own `post_engagement` and `page_engagement` totals already contain the individual interactions, and all of them are counted into `engagement`. Use these fields when you need a specific interaction, and `engagement` only as the coarse total it has always been. Meta-only; other platforms leave these at 0.
        - `postEngagement` integer — Meta's own post-engagement total (`post_engagement`).
        - `pageEngagement` integer — Meta's own page-engagement total (`page_engagement`).
        - `reactions` integer — Reactions on the ad's post (`post_reaction`).
        - `comments` integer — Comments on the ad's post.
        - `shares` integer — Shares of the ad's post. Meta reports these under the action type literally named `post`.
        - `saves` integer — Saves of the ad's post (`onsite_conversion.post_save`).
        - `pageLikes` integer — New Page likes attributed to the ad (`like`).
        - `videoViews` integer — 3-second video views (`video_view`). For completion-based counts use `videoThruplayWatchedActions`.
        - `linkClicks` integer — Attributed link clicks (`link_click`). This is the attribution-window count, which differs from the in-session `inline_link_clicks` reported by `GET /v1/ads/{adId}/analytics`.
      - `lastSyncedAt` string, date-time — Present on individual ads only, not on campaign aggregations
    - `platformAdId` string
    - `platformAdAccountId` string
    - `platformCampaignId` string
    - `platformAdSetId` string
    - `campaignName` string
    - `adSetName` string
    - `platformObjective` string, nullable — Raw Meta campaign objective (e.g. OUTCOME_SALES, OUTCOME_LEADS, OUTCOME_TRAFFIC). Only present for Meta ads.
    - `optimizationGoal` string, nullable — What the delivery system optimizes for, at ad-set level. The value space depends on `platform`: - Meta: ad set `optimization_goal` (e.g. OFFSITE_CONVERSIONS, VALUE, LEAD_GENERATION, LINK_CLICKS). - LinkedIn: the campaign's EFFECTIVE `optimizationTargetType`, refreshed from LinkedIn on every sync rather than echoing what was passed on create. `NONE` means manual bidding, and it is a real value, not missing data. Auto-bid values are MAX_IMPRESSION / MAX_CLICK / MAX_CONVERSION / MAX_VIDEO_VIEW / MAX_LEAD / MAX_REACH; target-cost values are TARGET_COST_PER_CLICK / TARGET_COST_PER_IMPRESSION / TARGET_COST_PER_VIDEO_VIEW; cost-cap values are the CAP_COST_AND_MAXIMIZE_* family.
    - `costType` string, nullable — LinkedIn only. The campaign's EFFECTIVE cost model (billing event) as applied by LinkedIn, refreshed on every sync rather than echoing what was passed on create. One of `CPM` (cost per thousand impressions), `CPC` (cost per click) or `CPV` (cost per video view). On LinkedIn this is the axis that pairs with `bidAmount`; there is no `bidStrategy`. For campaign type SPONSORED_INMAILS, `CPM` bills as cost-per-send x 1000. `null` for non-LinkedIn ads.
    - `servingStatuses` string[] — LinkedIn only. Why the parent campaign is (or is not) delivering, verbatim from LinkedIn. A campaign can report `status: ACTIVE` and still serve nothing; this array is what says so. - `[]` means no serving data: a non-LinkedIn ad, or a LinkedIn ad not yet re-synced. - `["RUNNABLE"]` means the campaign is eligible to serve. - Anything else is a hold. Known values include ACCOUNT_SERVING_HOLD, ACCOUNT_TOTAL_BUDGET_HOLD, ACCOUNT_END_DATE_HOLD, CAMPAIGN_START_DATE_HOLD, CAMPAIGN_END_DATE_HOLD, CAMPAIGN_TOTAL_BUDGET_HOLD, CAMPAIGN_AUDIENCE_COUNT_HOLD, CAMPAIGN_GROUP_START_DATE_HOLD, CAMPAIGN_GROUP_END_DATE_HOLD, CAMPAIGN_GROUP_TOTAL_BUDGET_HOLD, CAMPAIGN_GROUP_STATUS_HOLD and STOPPED. The list is open on purpose, so treat unrecognized values as holds rather than errors. The end-date and total-budget holds are terminal and surface as `status: completed`; the rest surface as `status: paused`. Note that a hold is not the only cause of zero delivery: with manual, target-cost or cost-cap bidding, a `bidAmount` of 0 stops delivery while `servingStatuses` still reads `["RUNNABLE"]`. Check `costType` / `bidAmount` / `optimizationGoal` as well.
    - `platformAdAccountName` string, nullable — Human-readable advertiser/account name (Meta `AdAccount.name`, TikTok `advertiser_name`, LinkedIn / X / Pinterest equivalents). Refreshed every sync so platform-side renames propagate within one cycle. `null` when the platform doesn't return a name or the sync hasn't run yet.
    - `platformCreatedAt` string, date-time, nullable — Platform-reported creation timestamp (Meta `created_time`, TikTok `create_time`). Distinct from `createdAt` which reflects when Zernio first synced the doc — for sort/filter by "when the ad was actually created on the platform", read this field. `null` for legacy ads synced before this field was added; aggregations fall back to `createdAt` in that case.
    - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS' — Meta bid strategy. Same enum applies at campaign and ad-set level; ad-set value (when set) overrides campaign-level. Cross-field rules: - `LOWEST_COST_WITHOUT_CAP` (default): auto-bid, forbids `bidAmount` and `roasAverageFloor`. - `LOWEST_COST_WITH_BID_CAP` / `COST_CAP`: require `bidAmount` (whole currency units). - `LOWEST_COST_WITH_MIN_ROAS`: requires `roasAverageFloor` (decimal multiplier, 2.0 = 2.0x). Source: facebook-business-sdk-codegen api_specs/specs/enum_types.json (`AdSet_bid_strategy`, `Campaign_bid_strategy`).
    - `bidAmount` number, nullable — Bid amount in WHOLE currency units of the ad account (USD: 5 = $5.00; JPY: 100 = ¥100). - Meta source: `bid_amount` on the ad set (smallest-denomination int, decoded here). Populated when bidStrategy is `LOWEST_COST_WITH_BID_CAP` or `COST_CAP`; `null` for auto-bid (`LOWEST_COST_WITHOUT_CAP`). - TikTok source: priority order `bid_price` -> `conversion_bid_price` -> `deep_cpa_bid` (whichever is set on the ad group). TikTok stores all three in whole currency units. - LinkedIn source: the campaign's EFFECTIVE `unitCost`, refreshed on every sync rather than echoing what was passed on create. Its meaning depends on the bidding mode implied by `optimizationGoal`: bid amount (manual), target cost, or cost cap. It pairs with `costType`, NOT with `bidStrategy`, which LinkedIn does not have. A value of `0` is a real, delivery- stopping configuration and not "unset", so do not gate this field on `bidStrategy` for LinkedIn ads. Source: facebook-business-sdk-codegen api_specs/specs/AdSet.json (`bid_amount`).
    - `roasAverageFloor` number, nullable — Minimum ROAS as a decimal multiplier (2.0 = 2.0x ROAS). Populated when bidStrategy is `LOWEST_COST_WITH_MIN_ROAS`. - Meta source: decoded from `bid_constraints.roas_average_floor` (Meta stores as fixed-point int × 10000; we return the decimal). - TikTok source: `roas_bid` on the ad group (already a decimal). Source: facebook-business-sdk-codegen api_specs/specs/AdCampaignBidConstraint.json.
    - `promotedObject` object, nullable — Meta promoted object containing conversion event details. Structure varies by objective. Only present for Meta ads.
      - `custom_event_type` string — Conversion event type (e.g. PURCHASE, LEAD, COMPLETE_REGISTRATION, ADD_TO_CART)
      - `pixel_id` string — Meta pixel ID
      - `page_id` string — Facebook page ID
      - `application_id` string — Facebook app ID
      - `product_set_id` string — Product catalog set ID
    - `creative` object, nullable — Platform-specific creative data. Fields vary by platform.
      - `thumbnailUrl` string, nullable — Primary thumbnail/image URL
      - `imageUrl` string — Alternative image URL
      - `videoId` string, nullable — Meta video ID for VIDEO-type ads. Null for non-video ads. Callers that need an embeddable MP4 can call GET /{videoId}?fields=source with the page access token.
      - `videoUrl` string, nullable — Public Facebook watch URL for VIDEO-type ads (https://www.facebook.com/watch/?v={videoId}). Null for non-video ads.
      - `objectType` string — Meta creative object_type (e.g. SHARE, VIDEO, PRIVACY_CHECK_FAIL, POST_DELETED). Use this to render state-aware previews — when Meta moderation strips image/video fields, only thumbnailUrl at 64x64 is available.
      - `objectStoryId` string, nullable — Meta creative `object_story_id` (the SHARE reference). Frequently absent — Meta omits it for SHARE creatives. Use effectiveObjectStoryId instead.
      - `effectiveObjectStoryId` string, nullable — Meta `effective_object_story_id` — `{pageId}_{postId}` of the Facebook post the ad's engagement (comments) lives on. Pass to GET /v1/ads?effectiveObjectStoryId= to map a Business-Manager-visible post back to this ad; GET /v1/ads/{adId}/comments resolves comments against it.
      - `pageId` string, nullable — Facebook Page backing the creative (Meta only). What the `pageId` filter on /v1/ads, /v1/ads/campaigns and /v1/ads/tree matches against. Absent for non-Meta ads and rare Meta creatives with no page signal.
      - `effectiveInstagramMediaId` string, nullable — Meta `effective_instagram_media_id` — the Instagram media ID of the boosted post the ad's engagement lives on. Pass to GET /v1/ads?effectiveInstagramMediaId= to map a Business-Manager-visible IG post back to this ad.
      - `instagramUserId` string, nullable — Meta `instagram_user_id` — the Instagram-scoped business ID that owns the boosted media.
      - `instagramPermalinkUrl` string, nullable — Meta `instagram_permalink_url` — public Instagram post URL of the boosted media.
      - `mediaUrls` string[] — All media URLs for this ad (carousel images, multiple assets). Populated for Meta (carousel child_attachments), Google Ads (responsive display marketing_images), and LinkedIn (multi-image posts).
      - `isServing` boolean, nullable — LinkedIn only. Whether LinkedIn is currently serving this specific creative. Complements the ad-level `servingStatuses`, which describes the parent campaign.
      - `servingHoldReasons` string[] — LinkedIn only. Why this specific creative is not being served. Empty when it is serving. A superset of the ad-level `servingStatuses`: it repeats the inherited campaign, campaign group and account holds AND adds creative-only causes such as UNDER_REVIEW, REJECTED, PROCESSING, PROCESSING_FAILED, FORM_HOLD (lead-gen-form creatives), REFERRED_CONTENT_QUALITY_HOLD, JOB_POSTING_ON_HOLD and JOB_POSTING_INVALID (job ads). Some values are format-specific and will never appear on other ad formats. The list is open, so treat unrecognized values as holds rather than errors.
      - `body` string — Ad copy/text
      - `googleHeadline` string — Google Ads headline
      - `googleDescription` string — Google Ads description
      - `linkUrl` string — Destination URL
      - `pinterestImageUrl` string
      - `pinterestTitle` string
      - `pinterestDescription` string
    - `targeting` object — The ad set's targeting (age, gender, geo, interests, placements, audience inclusions/exclusions). For ads created through Zernio this is the spec you supplied. For external ads (synced from Meta Ads Manager, `isExternal: true`) targeting lives at the ad set and isn't stored at ingest, so on the first `GET /v1/ads/{adId}` Zernio resolves it live from Meta and caches it on the ad; the value is then Meta's raw `targeting` shape (snake_case, e.g. `geo_locations`, `age_min`), the same object Ads Manager shows. May be absent if the ad set exposes no targeting or the lookup fails.
    - `schedule` object, nullable
      - `startDate` string, date-time
      - `endDate` string, date-time
    - `rejectionReason` string
    - `createdAt` string, date-time
    - `updatedAt` string, date-time
  - `message` string

## Other responses

- `400` — Missing required fields or invalid values
- `401` — Unauthorized
- `403` — Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
- `422` — Platform ads connection required (TikTok Ads, X Ads), missing linked account, or — for TikTok — the connected TikTok user is not authorized as an Identity on the target advertiser. Returned with code `ads_connection_required`; the message includes the actionable "TikTok Ads Manager → Assets → Identity" remediation step.

---

[API](https://skmtc.net/zernio/apis/zernio-api.md) · [All operations](https://skmtc.net/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/zernio/zernio-api/versions/51932b099b2f/schema)
