Meta Ads API breakdowns: the splits, and the three ways your totals go wrong
In short
Meta serves ad insights one breakdown at a time, and the failures are mostly silent. Three breakdowns need a per-account opt-in and answer without it using an empty HTTP 200. Reach never sums. And duplicate conversion entries, deduplicated rather than added, under-counted one live account by 42 percent.
If you ask the Meta Marketing API for a day of ad performance, you get one row. Ask it to split that day by placement, by age, by hour or by creative asset, and you are using a breakdown — and you are also, from that moment, responsible for a set of behaviours that the documentation either does not mention or describes incorrectly.
What follows is what nineteen breakdown passes look like in production, written from the connector that runs them and from the responses Meta actually returned. Dates are given wherever a claim came from a live call, because every one of these behaviours is free to change.
What is a breakdown in the Meta Ads API?
A breakdown is a query parameter on the /insights edge that divides each row of results by a dimension. breakdowns=publisher_platform turns one row for the day into one row per platform. breakdowns=age,gender turns it into one row per age bucket per gender. The metrics on each row — spend, impressions, clicks — are that slice’s share of the day.
Meta documents more than forty breakdown values on its Insights breakdowns reference. They are not interchangeable: each has its own retention window, its own set of metrics it will and will not carry, and its own rules about what it can be combined with. Covering them is nineteen separate requests, not one.
Grouping them by how they behave is more useful than listing them, because the behaviour is what decides how you store and read the result.
| Family | Example breakdowns | Divides all delivery? |
|---|---|---|
| Undivided | *(none)* | Yes — it is the whole day |
| Delivery splits | publisher_platform, age, gender, country, region, device_platform, impression_device, hourly | Yes |
| Frequency distribution | frequency_value | Yes, but only of reach |
| Creative assets | image_asset, video_asset, body_asset, title_asset, description_asset, call_to_action_asset, link_url_asset, ad_format_asset | No — only where Meta was left to vary that asset |
| Conditional | product_id, comscore_market | No — catalog delivery and US media markets only |
That last column is the one worth internalising. A delivery split returning nothing on a day that spent money is a problem. A creative-asset split returning nothing is the ordinary case, because an asset row only exists for an ad that handed Meta several assets and let it choose. Treating the two the same way produces either false alarms or missed data, depending on which way you guess.
Why can’t you request two breakdowns at once?
Because Meta mostly will not serve them. Asking for age alongside publisher_platform, or country alongside region, fails the whole request with error code 100. The reference explains why in one sentence: *“Due to storage constraints, only some permutations of breakdowns are available.”* It then publishes a table of the roughly thirty combinations that are.
The practical consequence is that full breakdown coverage is one request per breakdown over the same date range, not one clever request. Nineteen passes, each with its own checkpoint, its own throttle and its own failure — because a single combined request that Meta refuses takes every dimension down with it.
There is one pairing worth knowing about because it is accepted and should still be avoided: device_platform,impression_device. Meta serves it. But the two dimensions answer “which surface” and “which handset”, and the second is a refinement of the first — so the cross-tabulation produces rows that look like duplicates of each other to anyone who does not already know the relationship. Two separate passes are cheaper to read and no more expensive to fetch.
How far back does each breakdown go?
Three different windows, only one of which Meta states out loud. More importantly, all three fail differently at the edge, and two of the three failures are indistinguishable from an account that simply did not spend.
| Breakdowns | Window | One day past the edge |
|---|---|---|
| Everything | 37 months | A real error: (#3018) The start date of the time range cannot be beyond 37 months from the current date |
hourly_stats_aggregated_by_* | 13 months | HTTP 200 and an empty page — for the entire range |
frequency_value | 6 months | (#2) Service temporarily unavailable |
The hourly failure is the expensive one. Asked from 15 August 2025, an hourly request returned 136 rows. Asked from 14 August 2025 — one day earlier — it returned nothing at all, including for the days still comfortably inside retention. A window one day too wide does not lose its oldest day. It loses every day.
The frequency_value failure is expensive in a different way. (#2) Service temporarily unavailable is the error Meta returns during a genuine outage, so any sensible client classifies it as transient and retries. A client that retries a request which will never succeed spends error budget on every sync, forever — and Meta requires an app’s error rate to stay low to keep Advanced Access. (The asynchronous report path answers the same out-of-range request successfully, which is a difference to rely on for nothing.)
The observed edges are in fact more generous than the documented ones — frequency_value answered back to roughly eight months rather than six. Clamping to the documented figure is still the right call: the observed edge is undocumented, unannounced and free to move, and the cost of being a few weeks conservative is a few weeks of history, where the cost of being one day optimistic is the whole pass.
Why does a breakdown return 200 OK and no rows?
This is the central difficulty of the Insights edge. Meta almost never refuses a breakdown it will not serve. It answers with an empty data array and a 200 status, which is byte-for-byte what a correctly-configured account with no delivery looks like.
There are at least five distinct reasons for an empty page, and they need different responses:
| Cause | How to tell | What to do |
|---|---|---|
| No delivery that day | The undivided pass is also empty | Nothing. It is correct. |
Partial split (assets, product_id, comscore_market) | The undivided pass has rows; this split never divides all delivery | Nothing. Rank these rows, never present them as a share of the total. |
| Per-account opt-in not granted | The undivided pass has rows, and the split is one of three gated ones | Opt the account in. Until then, do not store the silence as a zero. |
Asset breakdown asked at level=account | Same request at level=campaign returns rows | Ask at campaign, ad set and ad. Account serves nothing. |
frequency_value requested without reach | The field list names no metric this breakdown serves | Keep reach in the field list. |
The three breakdowns that need a per-account opt-in
Since 6 August 2026, three breakdowns are gated behind a setting held on each individual ad account: impression_device, hourly_stats_aggregated_by_audience_time_zone and frequency_value. The gate does not inherit from the business. An account that has not been opted in is not refused — it is answered with an empty page, on dimensions that every impression has.
Meta’s own breakdowns reference says: *“If a synchronous request returns no results, the account administrator can opt in to this breakdown via Ads Manager.”* Verified live on 18 September 2026, that is not where the gate is. Ads Manager’s “Additional breakdowns” control governs a column in the reporting view, not the API.
The evidence for that is direct. Enabling impression_device under Ads Manager’s Additional breakdowns for one account reported *“Data available from Sep 19, 2026”* — while the Insights edge was already serving that same account eleven rows covering 20–28 August. And hourly_stats_aggregated_by_audience_time_zone still showed the un-enabled state in Ads Manager on an account where the edge answered with 47 rows.
The only gate anyone has documented for the API is POST /act_<id>/insights/feature-settings. It is enable-only: there is no call that turns it back off, which means an account you have already enabled cannot be put back into the withheld state to reproduce the behaviour.
The asymmetry between the two hourly clocks
Meta offers dayparting on two clocks — the advertiser’s timezone and the audience’s — and refuses to return them together. The reference says hourly breakdowns cannot carry deduplicated metrics. That is true of exactly one of the two.
| Metric | `..._by_advertiser_time_zone` | `..._by_audience_time_zone` |
|---|---|---|
impressions, spend, clicks | Served | Served |
reach | Omitted | Served |
frequency | Omitted | Served |
cpp | Omitted | Served |
unique_clicks | Omitted | Served |
| Needs per-account opt-in | No | Yes |
Which produces a genuinely confusing shape on an account that never opted in: the advertiser clock keeps working and the audience clock goes silent, so dayparting half-works and the half that fails is the half carrying reach.
Why don’t reach and frequency add up?
Because they are deduplicated per person. Reach is the number of distinct people who saw an ad. Somebody who saw it on Instagram and on Facebook is one person reached, counted once in the total and once in each platform’s row — so the platform rows sum to more than the total, and always will.
Reach, frequency and cost-per-reach cannot be summed across breakdown rows, across levels, or across days. A campaign’s weekly reach is not the sum of its seven daily reaches, and not the sum of its ads’ reaches. Each has to be requested for the exact grain you intend to display.
This is why the undivided pass is fetched rather than computed by summing a breakdown, and why a period figure — reach over the last 30 days — has to be a separate request over that window rather than an aggregation of stored daily rows. It is also the single most common defect in home-grown Meta reporting: spend and impressions aggregate correctly, so the pipeline looks right, and reach is wrong everywhere in a way nobody notices until a client asks how 40,000 people saw a campaign that reached 31,000.
Additive metrics are safe to aggregate, and worth confirming rather than assuming. device_platform was checked against the undivided reading on two days: 753 and 1,137 impressions, identical either way.
Why are your conversion counts too low?
Conversions arrive differently from everything else. They are not rows — they are an actions array hanging off a row, and action_breakdowns splits that array while leaving the row’s impressions, spend and reach exactly as the undivided pass reported them.
Two things follow, and both are easy to get wrong.
First, `action_type` is added whether you ask for it or not. The reference is explicit: *“If action_breakdowns parameter is not specified, action_type is implicitly added as the action_breakdowns.”* So a response split by action_destination still carries action_type on every entry. If you store the split entries *and* the rollup Meta already gave you, every conversion is counted twice.
Second, entries collide. Meta routinely returns several entries that share an action_type and an action_destination, with nothing in the response distinguishing them. On one live day, four separate post_engagement entries at destination="unknown" read 2, 12, 113 and 4.
Deduplicating those entries by keeping the first — the obvious implementation — under-counted one live account’s conversions by 42 percent: 179 stored against a true 308, with 16 of 110 daily action-type totals wrong. Summing colliding entries instead made both test accounts exactly right, 0 of 28 and 0 of 110 totals in error.
Requesting action_target_id as a third dimension narrows the collision but does not close it. It took one account’s indistinguishable entries from 42 down to 3; on the other it changed nothing at all, 12 before and 12 after.
One caveat on summing: only the count is additive. The cost-per-action and the monetary value are a rate and a total that Meta computed over a partition it declined to name, so adding those would invent a figure rather than recover one.
What this means if you are building it yourself
None of this is a reason not to build a Meta Ads integration. It is a list of the things that will be wrong for months if you do not know about them, because every one of them fails quietly.
- Treat an empty 200 as a finding, not a gap. Record *which* kind of empty you got — no delivery, a partial split, a missing opt-in — so a reader can tell the difference later. Storing a zero destroys that information permanently.
- Clamp every window before you send it, per breakdown, to the documented limit rather than the observed one. The hourly cliff costs you the whole range, not the overhang.
- Never sum reach, frequency or cost-per-reach. Request them at the grain you will display, including for every period a date picker offers.
- Sum colliding conversion entries; drop the implicit `action_type` rollup when you store a split, or your counts double.
- Ask for the full field list on every breakdown. Meta omits what it will not serve. An exclusion list you maintain by hand is a data loss waiting for a release.
- Give every breakdown its own checkpoint and its own retry. One pass failing should not cost you the other eighteen.
If you would rather not own that list, it is what the Meta Ads connector does — nineteen breakdown sets plus the undivided total, with the window clamping, the empty-answer taxonomy and the conversion summing already in place.
Questions people ask
How many breakdowns does the Meta Marketing API support?
Meta’s breakdowns reference documents more than forty values, including action breakdowns and SKAdNetwork dimensions. For ordinary ad reporting, nineteen distinct breakdown passes plus the undivided total cover the delivery, frequency, creative-asset and conditional dimensions.
Can you combine two breakdowns in one request?
Only a few pairs. Meta publishes a table of roughly thirty permitted permutations and rejects the rest with error code 100, citing storage constraints. In practice, full coverage means one request per breakdown over the same date range.
How far back does Meta Ads insights data go?
Thirty-seven months for most breakdowns, and Meta returns a clear error past that edge. Hourly breakdowns stop at thirteen months and frequency_value at six, and both fail silently or ambiguously rather than with a stated limit.
Why does my Meta Ads breakdown return no data?
Most often one of five causes: no delivery that day, a breakdown that only covers part of delivery, a missing per-account opt-in, an asset breakdown requested at account level, or a frequency_value request whose field list names no metric that breakdown serves.
Which Meta Ads breakdowns require an opt-in?
Since 6 August 2026, impression_device, hourly_stats_aggregated_by_audience_time_zone and frequency_value. The setting is per ad account and does not inherit from the business. An account without it receives an empty HTTP 200, not an error.
Can you add up reach across Meta Ads breakdowns?
No. Reach, frequency and cost-per-reach are deduplicated per person, so one person reached on two platforms appears in both platform rows and once in the total. Request them at the exact grain you intend to display.
Why are my Meta Ads conversion counts lower than Ads Manager’s?
Meta returns multiple action entries sharing an action_type and action_destination with nothing to distinguish them. Keeping only the first under-counted one live account by 42 percent. Sum colliding entries instead, but sum only the count, never the cost or value.
Sources
- Meta Marketing API — Breakdowns reference — The breakdown values, the permitted permutations, the implicit action_type, and the Ads Manager opt-in instruction
- Meta Marketing API — Ads Insights — The Insights edge itself, and the synchronous and asynchronous request paths
- Meta Graph API changelog — Versioned release notes and deprecation announcements for the Graph API and the APIs built on it
Read next
- The Instagram insights API: which metrics you get, and how it failsWhich metrics the Instagram insights endpoints return for media and for accounts, why one unsupported name takes down the whole request, and the 90-day limit.
- What an in-house Meta data pipeline actually costs to keep runningBuilding a Meta or Instagram integration takes a sprint. Keeping it correct is a standing commitment: a 60-day token clock, versions that expire silently, and seven failure modes.

