---
title: "Get ad details"
method: GET
path: "/v1/ads/{adId}"
tags: ["Ad Campaigns"]
---

# Get ad details

`GET /v1/ads/{adId}`

Returns an ad with its creative, targeting, status, and performance metrics.

The `{adId}` path segment accepts any identifier dialect Zernio indexes for the ad:
- the Zernio internal `_id` (24-char hex)
- Meta's numeric `platformAdId` (the value shipped in `comment.received` webhooks as `comment.ad.id`)
- the creative's `effective_object_story_id` (`{pageId}_{postId}` shape, Facebook side)
- the creative's `effective_instagram_media_id` (Instagram side)

Any of the four resolve to the same ad. Caller doesn't need a translation step.

## Path parameters

- `adId` string, required

## Response `200`

Ad details

- 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

## Other responses

- `401` — Unauthorized
- `404` — Resource not found

---

[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)
