Insights
Rolled-up metrics, demographic breakdowns, raw Graph queries, and async reports
Insights
There are three ways to read Meta performance data, in increasing order of power and cost.
| You want | Use | Notes |
|---|---|---|
| A dashboard: spend, CPC, CPM, conversions, ROAS per campaign / ad set / ad | GET /v1/ads/tree, GET /v1/ads, GET /v1/ads/{adId} | Served from Zernio's synced metrics. Pre-aggregated and rolled up, no Meta call on the request path. |
| One dimension split out (age, placement, creative asset) | GET /v1/ads/{adId}/analytics, GET /v1/ads/campaigns/{campaignId}/analytics | One Meta call per requested dimension. Cross-platform: the same endpoint serves LinkedIn's firmographic pivots. |
| Arbitrary Meta fields, breakdowns and filters | GET /v1/ads/insights | Raw Graph passthrough. Anything Meta's Insights API can answer. |
| The same, over a long range or at agency scale | POST /v1/ads/insights/reports | Meta runs it asynchronously, you poll for the result. |
Rolled-up metrics
Every metrics object on /v1/ads/tree, /v1/ads/campaigns and /v1/ads/{adId} carries the standard counters plus Meta's monetary fields. The full field list, including how roas is recomputed at each level instead of averaged, is documented under ROAS and revenue-per-event.
Demographic and placement breakdowns
GET /v1/ads/{adId}/analytics?breakdowns=... splits an ad's metrics by one or more dimensions. Meta serves breakdowns off any insights node, so the campaign-level GET /v1/ads/campaigns/{campaignId}/analytics takes the same parameter with no per-ad fan-out.
const { data } = await zernio.adinsights.getAdAnalytics({
path: { adId: 'AD_ID' },
query: { breakdowns: 'age,gender,publisher_platform' },
});data = client.ad_insights.get_ad_analytics(
ad_id="AD_ID",
breakdowns="age,gender,publisher_platform",
)curl "https://zernio.com/api/v1/ads/AD_ID/analytics?breakdowns=age,gender,publisher_platform" \
-H "Authorization: Bearer YOUR_API_KEY"Meta dimensions:
| Group | Values |
|---|---|
| Demographics | age, gender, country, region |
| Placement | publisher_platform, platform_position, device_platform, impression_device |
| Creative asset | video_asset, image_asset, body_asset, title_asset |
placement is not a Meta dimension. Meta rejects it, so Zernio returns a 400 listing the supported values with a pointer: use publisher_platform (facebook / instagram / audience_network) or platform_position (feed / story / reels). Any other unknown dimension also returns a 400 rather than being silently ignored.
Creative-asset breakdowns are what make dynamic creative and carousel results readable: they attribute spend and results to the individual image, video, body text or headline Meta served.
Flexible queries
GET /v1/ads/insights is a live raw-Graph query. You choose the fields, the breakdowns, the filters, the node and the row granularity.
const { data } = await zernio.adinsights.queryAdInsights({
query: {
accountId: 'ACCOUNT_ID',
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,
},
});
// data.paging.after -> pass back as `after` for the next pageimport json
data = client.ad_insights.query_ad_insights(
account_id="ACCOUNT_ID",
object_id="act_1234567890",
level="ad",
fields="ad_id,ad_name,spend,frequency,website_purchase_roas",
filtering=json.dumps([{"field": "spend", "operator": "GREATER_THAN", "value": 0}]),
date_preset="last_30d",
limit=100,
)curl -G "https://zernio.com/api/v1/ads/insights" \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "accountId=ACCOUNT_ID" \
--data-urlencode "objectId=act_1234567890" \
--data-urlencode "level=ad" \
--data-urlencode "fields=ad_id,ad_name,spend,frequency,website_purchase_roas" \
--data-urlencode 'filtering=[{"field":"spend","operator":"GREATER_THAN","value":0}]' \
--data-urlencode "datePreset=last_30d" \
--data-urlencode "limit=100"| Parameter | Notes |
|---|---|
objectId | Any insights-capable node: act_<n>, a campaign id, an ad set id, or an ad id. |
level | Row granularity (account, campaign, adset, ad), set independently of objectId. Querying an account at level: ad is the normal agency shape. |
fields | Comma-separated Meta field names, forwarded verbatim. |
breakdowns | Comma-separated Meta breakdown names, forwarded verbatim. |
filtering | A JSON-encoded array of Meta filter clauses (field, operator, value). |
datePreset or fromDate + toDate | Mutually exclusive. Dates are YYYY-MM-DD. |
timeIncrement | Days per row (1-90), or monthly, or all_days. |
limit / after | Page size (max 500, default 25) and the cursor. |
Validation is Meta's, and so are the errors. We don't keep a copy of Meta's field catalogue, which would go stale. An unknown field or an invalid field/breakdown combination returns a 400 carrying Meta's own message, and Meta's messages usually enumerate the valid values, which makes them a better reference than any table we could publish.
Paging returns paging.after only, never a next URL. Meta embeds the raw access token in the next URLs it generates, so Zernio strips them. Take after from the response and pass it back as the after parameter.
Attribution parameters
These apply to both the live query and the async reports, and they change how actions[] is counted and segmented:
| Parameter | Notes |
|---|---|
actionBreakdowns | Segments actions[], for example action_type,action_destination. |
actionAttributionWindows | Comma-separated on the query endpoint (7d_click,1d_view), a native array in the report body. dda and default are also accepted. |
actionReportTime | impression, conversion or mixed. |
useUnifiedAttributionSetting | true 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 date 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 with a reportRunId.
// 1. Submit
const { data: run } = await zernio.adinsights.createAdInsightsReport({
body: {
accountId: 'ACCOUNT_ID',
objectId: 'act_1234567890',
level: 'ad',
fields: 'ad_id,spend,actions',
actionAttributionWindows: ['7d_click', '1d_view'],
useUnifiedAttributionSetting: true,
fromDate: '2026-01-01',
toDate: '2026-06-30',
},
});
// 2. Poll until Meta finishes
let status = '';
do {
const { data } = await zernio.adinsights.getAdInsightsReport({
path: { reportRunId: run.reportRunId },
query: { accountId: 'ACCOUNT_ID', limit: 500 },
});
status = data.asyncStatus; // Meta's async_status, verbatim
if (status === 'Job Completed') console.log(data.rows, data.paging?.after);
} while (status !== 'Job Completed' && status !== 'Job Failed');run = client.ad_insights.create_ad_insights_report(
account_id="ACCOUNT_ID",
object_id="act_1234567890",
level="ad",
fields="ad_id,spend,actions",
action_attribution_windows=["7d_click", "1d_view"],
use_unified_attribution_setting=True,
from_date="2026-01-01",
to_date="2026-06-30",
)
while True:
data = client.ad_insights.get_ad_insights_report(
report_run_id=run.report_run_id,
account_id="ACCOUNT_ID",
limit=500,
)
if data.async_status in ("Job Completed", "Job Failed"):
break# 1. Submit
curl -X POST "https://zernio.com/api/v1/ads/insights/reports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "ACCOUNT_ID",
"objectId": "act_1234567890",
"level": "ad",
"fields": "ad_id,spend,actions",
"actionAttributionWindows": ["7d_click", "1d_view"],
"useUnifiedAttributionSetting": true,
"fromDate": "2026-01-01",
"toDate": "2026-06-30"
}'
# 2. Poll (same call pages the results once complete)
curl "https://zernio.com/api/v1/ads/insights/reports/REPORT_RUN_ID?accountId=ACCOUNT_ID&limit=500" \
-H "Authorization: Bearer YOUR_API_KEY"GET /v1/ads/insights/reports/{reportRunId} returns Meta's async_status verbatim while the job runs, and once it reads Job Completed the same call pages the results with limit and after.
Report runs are stateless on our side. Meta owns the job handle, so the reportRunId is all you need to keep. There's no expiry we control and no cleanup call: hold the id, poll when you want the data.