Zernio
Zernio
PlatformsMeta Ads

Build

CampaignsAd SetsCreativesCreative LibraryPreviews

Target

TargetingCustom Audiences

Ad types

Boost a PostCreative TestingConversion CampaignsCatalog AdsMessaging & Call AdsClick-to-WhatsApp AdsLead Gen FormsReach & Frequency

Measure

InsightsMeta PixelsConversions APIAd URL Tracking Tags

Operate

Duplicate & LifecycleAccount & Ops ReadsAd LibraryAd CommentsEnums, Limits & Error Codes
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Meta Ads

Insights

Read Meta ad performance: rolled-up metrics, demographic and placement breakdowns, live Graph queries and async report runs.


Read Meta performance data at increasing depth, from the metrics already synced onto the campaign tree to a raw Graph query you compose yourself. Pick the shallowest one that answers the question: the synced metrics cost no Meta call, the flexible query costs one per request, and a report run is a job Meta schedules.

Which endpoint to use

You wantCallWhat it costs
A dashboard: spend, CPC, CPM, conversions, ROAS per campaign, ad set or adGET /v1/ads/tree, GET /v1/ads, GET /v1/ads/{adId}Nothing. Served from Zernio's synced metrics, pre-aggregated and rolled up.
A daily series for one account: spend or conversions per calendar dayGET /v1/ads/timelineNothing. The same synced rows, one per day, instead of GET /v1/ads/tree once per day.
One dimension split out (age, placement, creative asset)GET /v1/ads/{adId}/analytics, GET /v1/ads/campaigns/{campaignId}/analyticsOne Meta call per requested dimension.
Arbitrary Meta fields, breakdowns and filtersGET /v1/ads/insightsOne live Graph call.
The same over a long range, or at agency scalePOST /v1/ads/insights/reportsA Meta job you poll.

GET /v1/ads/timeline takes accountId and an optional fromDate, toDate (last 90 days by default, 730 at most) and returns one row per calendar day with spend, impressions, reach, clicks, conversions and the derived ctr, cpc, cpm and roas. Reach is de-duplicated within a day only, so never sum it across days. A range reaching further back than the ingested history answers 202 with backfillPending: true and the covered part; repeat the call until it returns 200.

Every metrics object on the tree, the campaign list and GET /v1/ads/{adId} carries the standard counters plus Meta's monetary fields, and each ad node carries 3 separate status fields. Both sets are defined below.

ROAS and revenue per event

Every metrics object on GET /v1/ads/tree, GET /v1/ads/campaigns and GET /v1/ads/{adId} carries 3 monetary fields next to the actions and conversions counts:

FieldTypeMeaning
actionValues{[action_type]: number}Monetary mirror of actions, from Meta's action_values[], in the ad account's currency (the campaign node's currency). Populated for the action types Meta reports values on (purchases, AddToCart with value).
purchaseValuenumberSum of purchase-type action values, picked from actionValues with the same priority as conversions (offsite_conversion.fb_pixel_purchase, then omni_purchase, then purchase). Same unit as spend.
roasnumberpurchaseValue / spend. Recomputed from the summed numerator and denominator at ad set and campaign level, never averaged across children. Equals Meta's purchase_roas under default attribution.

A campaign rollup for a purchase campaign:

"metrics": {
  "spend": 493.39,
  "purchaseValue": 2456.78,
  "roas": 4.98,
  "conversions": 42,
  "costPerConversion": 11.75,
  "actions": { "offsite_conversion.fb_pixel_purchase": 42, "add_to_cart": 138, "link_click": 1205 },
  "actionValues": { "offsite_conversion.fb_pixel_purchase": 2456.78, "add_to_cart": 4230.50 }
}

For cost per AddToCart or cost per lead, read costPerAction[key], or divide actions[key] by spend. Prefer the offsite_conversion.fb_pixel_* keys: Meta reports the same conversion under a pixel key, an omni_* key and a canonical key at once.

Status axes on ads

Each ad node carries 3 status fields. Conflating them is the usual dashboard bug:

FieldAnswersValues
statusIs it delivering?Derived from Meta's effective_status, so it inherits ancestor pauses: an ACTIVE ad under a PAUSED campaign reads paused. An ad whose campaign or ad set schedule has ended reads completed.
configuredStatusWhat did you set?The ad's own on/off toggle, unaffected by ancestors.
reviewStatusWhat does Meta's review say?in_review, approved, rejected, with_issues. A rejected ad and an ad you paused are different problems.

Demographic and placement breakdowns

GET /v1/ads/{adId}/analytics with breakdowns splits one ad's metrics by one or more dimensions. Meta serves breakdowns off any insights node, so GET /v1/ads/campaigns/{campaignId}/analytics takes the same parameter with no per-ad fan-out.

import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: split } = await zernio.adinsights.getAdAnalytics({
  path: { adId: '66d4a1b2c3e4f5a6b7c8d9e2' },
  query: { breakdowns: 'age,gender' }
});

console.log(split.analytics.breakdowns.age);

Response (200), trimmed to one row per dimension:

{
  "ad": { "id": "66d4a1b2c3e4f5a6b7c8d9e2", "name": "Spring sale - US feed", "platform": "facebook", "currency": "USD" },
  "analytics": {
    "summary": { "spend": 493.39, "impressions": 88210, "clicks": 1205, "ctr": 1.37, "cpc": 0.41, "cpm": 5.59 },
    "breakdowns": {
      "age": [{ "age": "25-34", "spend": 210.40, "impressions": 39100, "clicks": 611 }],
      "gender": [{ "gender": "female", "spend": 268.90, "impressions": 47320, "clicks": 702 }]
    }
  }
}

analytics.breakdowns is keyed by the dimension you asked for. analytics.summary is the same ad totalled, and fromDate and toDate set the range (90 days back by default, 730 days at most).

GroupValues
Demographicsage, gender, country, region
Placementpublisher_platform, platform_position, device_platform, impression_device
Creative assetvideo_asset, image_asset, body_asset, title_asset

placement is not a Meta dimension. Zernio returns a 400 listing the supported values rather than passing it on: use publisher_platform (facebook, instagram, audience_network) or platform_position (feed, story, reels). Any other unknown dimension is a 400 too, never a silent drop.

The creative-asset breakdowns are what make creative testing and carousel results readable: they attribute spend and results to the individual image, video, body text or headline Meta served. The same endpoint serves LinkedIn's firmographic pivots (job_title, seniority, industry and the rest) on a LinkedIn ad.

Flexible queries

GET /v1/ads/insights is a live Graph query where you choose the fields, breakdowns, filters, node and row granularity.

const { data: rows } = await zernio.adinsights.queryAdInsights({
  query: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    objectId: 'act_1234567890',
    level: 'ad',
    fields: 'ad_id,ad_name,spend,frequency,website_purchase_roas',
    filtering: JSON.stringify([{ field: 'spend', operator: 'GREATER_THAN', value: 0 }]),
    datePreset: 'last_30d',
    limit: 100
  }
});

const nextCursor = rows.paging.after;

Response (200), rows in Meta's raw shape:

{
  "objectId": "act_1234567890",
  "data": [
    {
      "ad_id": "120260000000000000",
      "ad_name": "Spring sale - US feed",
      "spend": "493.39",
      "frequency": "1.84",
      "website_purchase_roas": [{ "action_type": "offsite_conversion.fb_pixel_purchase", "value": "4.98" }]
    }
  ],
  "paging": { "after": "MjM4NDI2..." }
}
ParameterMeaning
objectIdAny insights-capable node: act_<n>, a campaign id, an ad set id or an ad id.
levelRow granularity (account, campaign, adset, ad), set independently of objectId. An account queried at level=ad is the normal agency shape.
fieldsComma-separated Meta field names, forwarded verbatim.
breakdownsComma-separated Meta breakdown names, forwarded verbatim.
filteringA JSON-encoded array of Meta filter clauses (field, operator, value).
datePreset, or fromDate with toDateMutually exclusive. Dates are YYYY-MM-DD.
timeIncrementDays per row (1 to 90), or monthly, or all_days.
limit, afterPage size (500 at most, 25 by default) and the cursor.

Validation belongs to Meta, and so do the errors. Zernio keeps no copy of Meta's field catalogue, which would go stale, so an unknown field or an invalid field and breakdown combination returns a 400 carrying Meta's own message, and those messages usually enumerate the valid values.

Paging returns paging.after and never a next URL: Meta embeds a raw access token in the next URLs it generates, so Zernio strips them. Take after from the response and send it back as the after parameter.

Attribution parameters

These apply to the live query and to report runs, and they change how actions[] is counted and segmented:

ParameterMeaning
actionBreakdownsSegments actions[], for example action_type,action_destination.
actionAttributionWindowsComma-separated on the query endpoint (7d_click,1d_view), a native array in the report body. dda and default are accepted too.
actionReportTimeimpression, conversion or mixed.
useUnifiedAttributionSettingtrue counts using each ad set's own attribution setting.

With actionAttributionWindows set, every action row comes back keyed per window:

{ "action_type": "purchase", "value": "132", "1d_view": "119", "7d_click": "13" }

Async reports

For long ranges or account-wide pulls, submit the same query as a job. POST /v1/ads/insights/reports takes the query as a JSON body with native types (windows as an array, booleans as booleans) and returns 202.

curl -X POST "https://zernio.com/api/v1/ads/insights/reports" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "objectId": "act_1234567890",
    "level": "ad",
    "fields": "ad_id,spend,actions",
    "actionAttributionWindows": ["7d_click", "1d_view"],
    "useUnifiedAttributionSetting": true,
    "fromDate": "2027-01-01",
    "toDate": "2027-06-30"
  }'

Response (202):

{ "reportRunId": "6234567890789", "status": "Job Not Started" }

Then poll GET /v1/ads/insights/reports/{reportRunId}. The same call carries the rows once the job finishes. Sleep between polls: Meta schedules the job, and the ranges and account sizes this path exists for take it well past the first read, so start at 5 seconds and double the wait up to a minute. percentCompletion is what to show a user while it runs.

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

let report;
let waitMs = 5000;
do {
  await sleep(waitMs);
  waitMs = Math.min(waitMs * 2, 60000);
  ({ data: report } = await zernio.adinsights.getAdInsightsReport({
    path: { reportRunId: '6234567890789' },
    query: { accountId: '66b2e19d8c3f5a7e9d0b1c2d', limit: 500 }
  }));
  console.log(report.status, report.percentCompletion);
} while (!['Job Completed', 'Job Failed', 'Job Skipped'].includes(report.status));

console.log(report.data, report.paging.after);

Response (200), once complete:

{
  "reportRunId": "6234567890789",
  "status": "Job Completed",
  "percentCompletion": 100,
  "dateStart": "2027-01-01",
  "dateStop": "2027-06-30",
  "data": [{ "ad_id": "120260000000000000", "spend": "493.39" }],
  "paging": { "after": "MjM4NDI2..." }
}

status is Meta's async_status verbatim, so it moves through Job Not Started, Job Started, Job Running and Job Completed, with percentCompletion alongside. Job Failed and Job Skipped are both terminal: stop polling and submit the report again, and treat any loop that waits only for Job Completed as one that can spin forever. Report runs hold no state on Zernio's side: Meta owns the job handle, so the reportRunId is the only thing to keep, and there is no expiry to manage and no cleanup call.

Common errors

A 400 on a flexible query is Meta rejecting a field or a combination:

{
  "error": "(#100) breakdowns[0] must be one of the following values: ...",
  "type": "platform_error",
  "platform": "meta",
  "platformError": { "code": 100 }
}

Meta's message lists the values it will accept; pick one from it. A 501 means the account is not Meta or Google Ads, the only 2 platforms this endpoint serves.

Related

  • Campaigns: the rolled-up metrics on the tree.
  • Creative testing: what the creative-asset breakdowns are for.
  • Conversions: the events these numbers count.
  • Query insights and Create report run: every parameter.
Was this page helpful?

Reach & Frequency

Quote, reserve and buy a fixed-price Meta reach and frequency campaign with POST /v1/ads/rf-predictions and buyingType RESERVED.

Meta Pixels

Create, read, rename and share a Meta Pixel through the tracking tags API, and read its firing stats.

On this page

Which endpoint to useROAS and revenue per eventStatus axes on adsDemographic and placement breakdownsFlexible queriesAttribution parametersAsync reportsCommon errorsRelated