Ads Insights (Beta)
Ads Insights lets you read Meta advertising performance through the same Ads Uploader tools that launch your ads. Use the REST API from an integration, the CLI from a terminal or coding agent, or MCP from a connected AI assistant. All three surfaces use the same field catalog, request rules, normalized columns, and cursor paging.
Insights is read-only. A report does not change campaigns, budgets, ads, or delivery settings.
Every completed Insights query returns:
metawith the account, currency, timezone, effective date range, attribution, versions, row count, and warningscolumnsdescribing every returned key and data typerowscontaining normalized values, as objects by default in REST and the CLI, or arrays in column order withformat: "table"actionColumnsandactionRowswhen non-default action breakdowns are requestedpagingwith an opaqueaftercursor andhasMore
Quick Start
REST API
Set your API host, Ads Uploader bearer token, and ad account ID. For a single account, use the X-Ad-Account header; for multiple accounts, omit it and send accountIds in the body.
export ADS_API_URL="https://adsuploader.com"
export ADS_API_TOKEN="<YOUR_API_TOKEN>"
export ADS_ACCOUNT_ID="act_123"
List recommended fields:
curl --silent --show-error \
"$ADS_API_URL/api/v1/insights/fields?recommended=true&limit=100" \
--header "Authorization: Bearer $ADS_API_TOKEN"
Find the top ads by spend for the last 7 days:
curl --silent --show-error \
--request POST "$ADS_API_URL/api/v1/insights/query" \
--header "Authorization: Bearer $ADS_API_TOKEN" \
--header "X-Ad-Account: $ADS_ACCOUNT_ID" \
--header "Content-Type: application/json" \
--data '{
"level": "ad",
"fields": ["ad_id", "ad_name", "spend", "impressions", "clicks"],
"datePreset": "last_7d",
"sort": { "field": "spend", "direction": "desc" },
"limit": 10
}'
CLI
Run ads login once, then set a default account with ads account act_123 or pass --account on each query.
ads insights:fields --recommended
ads insights --level ad --fields ad_id,ad_name,spend,impressions,clicks --preset last_7d --sort spend:desc --limit 10
The default output is a readable table. Use --json for the lossless REST response, including exact IDs, nulls, action keys, column metadata, and cursors.
ads insights --fields spend,impressions,clicks --preset last_7d --json
MCP
Connect the Ads Uploader MCP server, then ask your agent questions such as:
- "Which ads spent the most in the last 7 days in account act_123?"
- "Show daily purchase CPA for campaign 1201 over the last 30 days."
- "Show a placement breakdown for ad set 55 over the last 14 days."
- "Compare last week with the week before."
The agent discovers fields by search or group, then calls ads_query_insights with an explicit accountId or accountIds. MCP defaults to format: "table"; each row is an array ordered by columns. Use format: "objects" for keyed rows.
Field Discovery
Use the field catalog before guessing a Meta field ID. GET /api/v1/insights/fields, ads insights:fields, and ads_list_insight_fields return the same catalog version and supported query options.
ads insights:fields --search purchase
ads insights:fields --kind action_family
ads insights:fields --kind breakdown
Unfiltered discovery returns a compact group index (id, label, fieldCount) and recommended fields. Use search, group, kind, or explicit comma-separated ids for full entries, including notes about availability and uncertain Meta types. Existing recommended, limit, and offset filters still work. Catalog entries include id, label, kind, dataType, shape, group, recommended, and deprecated. The response also lists supported levels, date presets, time increments, breakdowns, attribution values, sorting rules, execution modes, and sync/async limits.
Add X-Ad-Account to REST discovery, accountId in MCP, or --account in the CLI to include custom conversion definitions and labels. Definitions refresh after 24 hours. Without an account, discovery only needs Ads Uploader authentication.
Request metric, dimension, action-family, and friendly alias IDs through fields. Request breakdown dimensions through breakdowns, not fields.
Friendly Metrics and Campaign Goals
Use friendly metrics for business questions, for example fields: ["spend", "leads", "cost_per_lead", "purchases", "roas"]. Each alias counts as one requested field. Its source fields are fetched automatically and only shown when explicitly requested. Values use Meta's attribution-default value, even when actionTypes filters a separately requested native action family.
Omitted action counts and values are 0 on rows with delivery (impressions > 0) and null otherwise. A positive matching count without an action value stays null. Native result indicators with empty values are 0 on delivered rows; cost per result stays null for zero results. Division returns null when its numerator is missing or its denominator is missing or zero. Cost metrics, CTR and ROAS are not additive. Total actions are never presented as total conversions.
| Metric | Formula |
|---|---|
link_clicks | actions[link_click].value |
landing_page_views | actions[landing_page_view].value |
video_views_3s | actions[video_view].value |
post_engagements | actions[post_engagement].value |
page_engagements | actions[page_engagement].value |
post_reactions | actions[post_reaction].value |
post_comments | actions[comment].value |
post_shares | actions[post].value |
post_saves | actions[onsite_conversion.post_save].value |
page_likes | actions[like].value |
leads | actions[lead].value |
leads_on_facebook | actions[onsite_conversion.lead_grouped].value |
leads_website | actions[offsite_conversion.fb_pixel_lead].value |
purchases | actions[omni_purchase].value |
website_purchases | actions[offsite_conversion.fb_pixel_purchase].value |
add_to_cart | actions[omni_add_to_cart].value |
initiate_checkout | actions[omni_initiated_checkout].value |
add_payment_info | actions[offsite_conversion.fb_pixel_add_payment_info].value |
complete_registration | actions[omni_complete_registration].value |
view_content | actions[omni_view_content].value |
app_installs | actions[omni_app_install].value |
messaging_conversations_started | actions[onsite_conversion.messaging_conversation_started_7d].value |
purchase_value | action_values[omni_purchase].value |
website_purchase_value | action_values[offsite_conversion.fb_pixel_purchase].value |
cost_per_link_click | spend / link_clicks |
cost_per_landing_page_view | spend / landing_page_views |
cost_per_lead | spend / leads |
cost_per_purchase | spend / purchases |
cost_per_add_to_cart | spend / add_to_cart |
cost_per_complete_registration | spend / complete_registration |
link_ctr | 100 * link_clicks / impressions |
roas | purchase_value / spend |
website_roas | website_purchase_value / spend |
add_payment_info covers website payment-info events. Other grouped conversion aliases use Meta's documented all-channel action types. These mappings follow Meta's AdsActionStats reference.
For custom conversions, request custom_conversion.123, custom_conversion_value.123, or cost_per_custom_conversion.123. The formulas are the count or value of offsite_conversion.custom.123, or spend divided by that count. IDs must contain digits. Run account-scoped discovery first to show the known conversion name in the column label.
For "how is this campaign doing on its goal", request native results, cost_per_result, and result_rate. Meta returns indicators with attributed values, and the goal can differ between campaigns. Indexed columns such as results.0.indicator, results.0.values.0.value, and results.0.values.0.attribution_windows.0 preserve multiple indicators and windows. Do not add them together. See catalog notes for nested-shape and account-availability caveats. When native results are absent, campaign, ad set and ad rows can derive results and cost_per_result from ad set optimization settings and delivery, with result_source: "derived" and result_indicator. At ad set and ad level, only the matching delivery row determines the result. Incomplete data and breakdown slices remain unknown.
Value Filters
where accepts up to five objects with field, operator, and value. Filters combine with the existing campaign, ad set, and ad ID filters. excludeZeroImpressions defaults to true; false includes zero rows. Comparisons test delivery in either period after joining.
- Numeric fields:
spend,impressions,clicks,reach,frequency,ctr,cpc,cpm, andinline_link_clicks. Usegt,gte,lt,lte,eq, orneqwith a number, orin/not_inwith an array of numbers. - Names:
campaign.name,adset.name, andad.name. Useeq,neq,contains,not_contains, orstarts_withwith a string;in/not_intake arrays of strings. - Effective status:
campaign.effective_status,adset.effective_status, andad.effective_status. Usein/not_inwith arrays of Meta statuses such asACTIVEorPAUSED. - Objective:
campaign.objectiveusesinwith an array of Meta objectives, such asOUTCOME_SALES.
{
"fields": ["spend", "purchases", "roas"],
"where": [{ "field": "spend", "operator": "gt", "value": 10 }],
"excludeZeroImpressions": true
}
In the CLI, quote each repeatable expression: --where "spend>10" --where "campaign.name~Summer". Supported shorthand operators are >, >=, <, <=, =, !=, and ~ (contains). Use REST or MCP for array-valued filters.
Date Ranges and Time Increments
Choose either datePreset or timeRange, never both. If neither is supplied, the default is last_30d. Explicit ranges are inclusive and use the ad account timezone returned in meta.accountTimezone.
{
"timeRange": {
"since": "2026-08-01",
"until": "2026-08-07"
}
}
timeIncrement | Result buckets |
|---|---|
all_days | One aggregate bucket for the full range. This is the default. |
daily | One bucket per day, with a maximum range of 90 days in sync mode or 400 days in auto/async mode. |
weekly | Seven-day buckets. |
monthly | Calendar-month buckets. |
meta.dateRange describes the whole requested range, not one bucket. Explicit ranges are echoed directly. For a preset with all_days, it is taken from Meta's returned row, so an empty result can have a null effective date range. For a preset with daily, weekly, or monthly buckets, it is the preset's full range in the ad account timezone. maximum and data_maximum have no fixed start, so bucketed results report the span of the returned rows.
Breakdowns
Breakdowns split reporting rows by a dimension. Discover the full list with ads insights:fields --kind breakdown. It includes demographics, placement, device, hourly time zones, frequency, DMA, product/catalog, conversion and landing destinations, and asset/media breakdowns.
Use one asset or media breakdown alone to inspect flexible or dynamic creative ads, for example title_asset, image_asset, video_asset, or media_type. Each hourly breakdown also must be used alone. Other requests allow at most two breakdowns. Object-valued assets become string columns such as title_asset.id, title_asset.text, and image_asset.url; plain string values remain supported.
Some breakdowns omit off-Meta action metrics. frequency_value, audience-time-zone hourly reporting, and impression_device may require Ads Manager opt-in or an async report for the account. Read catalog notes and use mode: "async" if appropriate.
For example, split delivery by publisher and placement:
ads insights --fields spend,impressions,clicks --preset last_14d --breakdown publisher_platform --breakdown platform_position
Meta does not support every field and breakdown combination. A combination that passes the Ads Uploader allowlist can still receive a safe 422 response when Meta rejects it. Remove a field or breakdown and try again.
Action Types and Flattened Columns
Meta returns fields such as actions, action_values, and cost_per_action_type as arrays. Ads Uploader flattens them into stable column keys using:
family.action_type.property
Request only the action types you need:
{
"fields": ["date_start", "date_stop", "actions", "cost_per_action_type"],
"actionTypes": ["offsite_conversion.fb_pixel_purchase"],
"datePreset": "last_30d",
"timeIncrement": "daily"
}
The response can include columns such as:
actions.offsite_conversion.fb_pixel_purchase.value
cost_per_action_type.offsite_conversion.fb_pixel_purchase.value
When explicit attribution windows are requested, action columns can also end in properties such as .1d_click or .7d_click.
null and 0 are different. 0 means Meta returned an observed zero. null means a requested action type or property had no returned value for that row. A missing scalar property was not returned at all. Never convert missing or null values into zero.
Action Breakdowns and Compact Output
actionBreakdowns defaults to ["action_type"], which keeps the flattened action columns described above. Other allowed dimensions include action_device, action_destination, carousel card ID/name, reaction, video sound/type, Canvas component name, and target ID. Discover their exact names in the catalog response's actionBreakdowns list. action_type is always retained.
With another action dimension, native action families move into actionRows, described by actionColumns. Each entry has a zero-based row index into the base rows, a family, breakdown values, and numeric values or attribution properties. The base rows omit the flattened action-family columns and include a warning explaining the separate table. Friendly aliases cannot be combined with non-default action breakdowns.
ads insights --fields spend,actions --action-breakdown action_device --json
ads insights --fields purchases,roas --where "spend>10" --format table --json
format: "table" keeps column metadata and represents both rows and actionRows as arrays in column order. Missing cells are represented as null; use objects when you need to distinguish a missing scalar key from explicit null. REST and CLI JSON default to objects; MCP defaults to table. Both formats share the same cached page. Job reads also accept format to override the original query's choice. The 2,000-cell limit includes both tables. MCP trims complete base rows together with their actions if a page exceeds its output budget; rerun a smaller page before following its cursor.
Attribution
Queries follow the ad account attribution setting by default. The effective choice is echoed in meta.attribution.
To use explicit windows, supply attribution.windows. This switches off the account setting for that query. Do not combine explicit windows with useAccountSetting: true.
{
"attribution": {
"windows": ["7d_click", "1d_view"],
"reportTime": "conversion"
}
}
Supported report times are conversion, impression, mixed, and lifetime. Results can differ from Ads Manager when the date range, account timezone, attribution windows, or report time do not match. Align all four before comparing totals.
Sorting and Paging
Sort by one native scalar field with asc or desc. Action-family fields, friendly aliases, and native result arrays are not sortable.
{
"sort": { "field": "spend", "direction": "desc" }
}
Each request fetches one bounded Meta page. It never fetches every page automatically. When another page exists, the response contains:
{
"paging": {
"after": "OPAQUE_CURSOR",
"hasMore": true
}
}
Keep the query unchanged and send the cursor in the next request's after field. In the CLI, use --after OPAQUE_CURSOR. In MCP, pass the cursor to ads_query_insights.after for synchronous pages. For completed async reports, use the job endpoint, ads insights:job --after, or ads_get_insights_job.after. Follow a cursor only when the next page is actually needed.
Limits
The field endpoint publishes the active limits with each catalog response.
| Limit | Value | Meaning |
|---|---|---|
maxFields | 30 | Maximum requested field IDs. |
maxBreakdowns | 2 | Maximum breakdown dimensions. |
maxCells | 2,000 | Maximum estimated or normalized cells in one page. |
maxDailyDays | 90 | Maximum daily range in sync mode. |
maxDailyDaysAsync | 400 | Maximum daily range in auto/async mode. |
defaultLimit | 25 | Default result rows per query. |
maxLimit | 100 | Maximum result rows per query. |
Reduce fields, action types, attribution windows, breakdowns, or limit when a query exceeds the cell cap.
Cache Freshness
Results are cached per user, account, query, and cursor. meta.cache reports hit, storedAt, ageSeconds, and ttlSeconds. The cache lifetime depends on the range end in the account timezone:
| Range end | Cache lifetime |
|---|---|
| Includes today | 10 minutes |
| 1 to 3 days ago | 15 minutes |
| 4 to 28 days ago | 1 hour |
| More than 28 days ago | 24 hours |
If the timezone is unavailable, ranges ending on or after UTC yesterday use the 10-minute lifetime. Pass fresh: true in REST or MCP, or --fresh in the CLI, to skip an existing cached result and refresh it. A concurrent refresh may supply the new result. Refreshed pages can still be cached. CLI tables show a note such as "Cached 3m ago" on a hit.
Async Report Jobs
mode supports auto (default), sync, and async. The rules below apply to single-account queries without comparison. Comparison and multi-account queries are synchronous only. The field catalog publishes modes and active limits.
autouses a synchronous request for smaller queries. Daily ranges over 90 and up to 400 days use an async report. Meta's "too much data" response also switches the query to async.synckeeps the 90-day daily limit and returns guidance if Meta cannot return the requested amount of data.asyncalways submits a report within the async limits. Daily quarter/year presets use async; dailymaximumanddata_maximumrequire an explicit range to prove the 400-day limit.
Submitting a report returns HTTP 202 with status: "pending", job.id, job.status, job.percentComplete, job.pollAfterSeconds, and request metadata. Follow that ID rather than submitting the query again:
GET /api/v1/insights/jobs/ij_<id>?waitSeconds=20&limit=25
Use the same bearer token identity and X-Ad-Account. waitSeconds is clamped to 0 through 20, with status checks every 3 seconds. HTTP 202 means status: "running"; HTTP 200 returns the usual meta, columns, rows, and paging, plus meta.job.status: "completed". Continue completed reports with the job endpoint and after, keeping the same job ID. Completed pages are cached for one hour. Job references expire after 24 hours. Failed reports return safe 502 INSIGHTS_JOB_FAILED; expired or inaccessible references return 404 INSIGHTS_JOB_NOT_FOUND.
The shared client exposes insights.query(body) and insights.job(jobId, { waitSeconds, after, limit, format }). Both make one attempt per request. The CLI follows pending reports automatically and prints progress to stderr. Use --no-wait to print the job ID and exit, or --no-wait --json for the pending JSON:
ads insights --mode async --fresh --no-wait --fields spend,impressions --preset last_30d
ads insights:job ij_<id> --wait 20 --json
ads insights:job ij_<id> --after OPAQUE_CURSOR --limit 25 --json
In MCP, pass mode and fresh to ads_query_insights. On a pending response, call ads_get_insights_job with the original accountId and jobId: job.id. Its default wait is 15 seconds, with a maximum of 20. Never rerun the query to check progress.
If the cache is temporarily unavailable, queries still run without it. Async jobs return 503 INSIGHTS_JOBS_UNAVAILABLE when report tracking is temporarily unavailable; try again shortly.
Rate Limits
Reporting and resource reads share a per-user burst limit of 20 requests per second, including cached responses, field discovery, job polling and existing campaign/ad set/ad lists. Excess requests return 429 RATE_LIMITED with Retry-After: 1. Each read operation also permits 600 requests per minute; other API operations keep their existing limits.
Fresh reporting calls wait for app capacity and then account capacity for up to 20 seconds in total, checking about every 200 ms. Waiting holds no slot. Defaults are 5 app calls, 1 Insights call and 1 new resource read per account. After the queue times out, META_READS_BUSY, INSIGHTS_ACCOUNT_BUSY or READS_ACCOUNT_BUSY returns 429 with Retry-After: 5. An async-eligible, first-page mode: "auto" query instead returns a queued job. Follow job.id through the job endpoint; do not resubmit the query. Background submission and subsequent polls respect Meta usage backoff and the Ads Uploader short-window read safety limit described below. Comparison and multi-account queries remain synchronous.
Identical queries run once. At most 10 duplicate requests may wait for the result; the next returns 429 TOO_MANY_DUPLICATE_REQUESTS with Retry-After: 2. Cache hits and async status checks consume no concurrency slot.
Fresh Meta rows count toward a monthly quota shared across accounts and read operations. The default is 1,000,000 rows for each subscription tier; admins are unlimited. Cached rows do not count again. Metadata and definition rows count too, while async submission and status checks do not add rows. Calls admitted before the limit is reached may take usage over the limit by their returned pages. Further fresh calls return 429 ROW_QUOTA_EXCEEDED until midnight UTC on the first day of the next month. Cached pages remain available.
Responses carry meta.quota = { rowsUsed, rowsLimit, resetsAt }. Unlimited admins have rowsLimit: null; unavailable usage is rowsUsed: null and enforcement fails open. CLI tables show a one-line note at 80% usage. Existing CLI list JSON shapes stay compatible.
Meta also applies its own account, user, application and Insights throttles. Honor Retry-After on 429 responses. For ROW_QUOTA_EXCEEDED, wait until meta.quota.resetsAt or the error's reset time. The client's existing 60-second automatic retry ceiling for other operations is unchanged. The shared API client, CLI and MCP make one request attempt; do not build retry loops.
Caveats
- Reach and frequency are non-additive. Do not sum them across rows or periods. CTR and other ratios should also be compared or recomputed from their source totals, not summed.
- Empty rows are a successful response. Zero-impression rows are excluded by default (
excludeZeroImpressions: true); setexcludeZeroImpressions: falseor use CLI--include-zero-impressionsto include them. Comparisons keep a row when either period has impressions above zero, so drops to zero remain visible. - Insights are not real-time. Recent performance usually refreshes in about 15 minutes, and Meta can revise it while attribution settles.
- Insights are generally final after 28 days.
- Currency, timezone, attribution, and effective dates are part of
meta. Keep them with exported or compared results. - This beta is intentionally bounded. There is no automatic fetch-all or CSV mode.
Comparing Periods
Use compare: { "type": "previous_period" } for an equal-length range ending the day before the current range. Use previous_year to subtract one year, clamping Feb 29 to Feb 28, or custom with its own timeRange:
{
"level": "ad",
"fields": ["ad_name", "spend", "purchases", "roas"],
"datePreset": "last_7d",
"compare": { "type": "custom", "timeRange": { "since": "2026-08-01", "until": "2026-08-07" } },
"include": ["status", "budget"],
"limit": 10
}
Meta's returned dates define the current range. If the current page is empty, a calendar resolver uses the account timezone. meta.compare reports type, current, and previous ranges. Each numeric column, including friendly aliases and ratios, gets adjacent .previous, .change (current minus prior), and .change_pct (percentage change) columns. Unmatched rows get null prior values. Change is null if either value is missing; percentage change is also null when the prior value is zero. No rows or periods are summed.
Rows match by entity ID and breakdown values. The prior query keeps the same fields, breakdowns, filters and attribution, restricted to current-page entity IDs at campaign, ad set or ad level. It reads at most 500 prior rows across three cursor pages. COMPARE_PARTIAL warns when prior coverage is incomplete. Prior data uses its own cache and the same date-based lifetime rules; fresh refreshes both periods.
Comparison requires timeIncrement: "all_days" (COMPARE_REQUIRES_ALL_DAYS). Async execution, including an automatic fallback, is rejected with COMPARE_NOT_ASYNC. maximum and data_maximum are rejected with COMPARE_UNSUPPORTED_PRESET; non-default action breakdowns return COMPARE_WITH_ACTION_BREAKDOWNS. Request validation returns HTTP 400 with these codes in details.errors; runtime errors use errorCode. Comparison columns count toward the 2,000-cell output bound.
ads insights --level ad --fields ad_name,spend,roas --preset last_7d --compare previous_period --include status,budget --limit 10
ads insights --fields spend,purchases --preset last_7d --compare previous_year --json
ads insights --fields spend --preset last_7d --compare-range 2026-08-01..2026-08-07 --json
Entity Context
Use include for context rather than separate list calls. Choose any of status, budget, schedule, setup, targeting, or creative. The response adds entities: { campaigns: {}, adsets: {}, ads: {} }, keyed by ID. Metadata is returned once per entity, separately from rows; meta.entityRefs is an array aligned with rows that links each row to its campaign, ad set and ad IDs. Automatically fetched IDs stay hidden from columns unless requested. Table format leaves entities unchanged.
| Category | Returned context |
|---|---|
status | Status, effective status and configured status; ads also include review feedback |
budget | Campaign/ad set daily, lifetime and remaining budgets, bid strategy, campaign spend cap and ad set bid amount |
schedule | Campaign start/stop times and ad set start/end times |
setup | Campaign objective, buying type and special categories; ad set optimization goal, billing event, promoted object and destination type |
targeting | Age, genders, all included/excluded geo dimensions with capped lists and counts, country-group labels, detailed-targeting groups, custom/excluded audiences and Advantage audience flag |
creative | Ad creative ID, name, title, body, link, call to action, thumbnail/image URLs, post IDs, Instagram permalink, URL tags and ad preview link; dynamic asset counts for bodies, titles, images and videos |
Budgets use the account currency's major units, with raw Meta strings retained as <field>_minor. USD 12345 minor units becomes 123.45; JPY 12345 remains 12345. Unknown currencies keep the minor string and a null major value. Targeting and creative assets are summaries; full targeting and asset feed specs are not returned. Targeting summaries use geo.countries, country_groups ({ key, label }, including European Economic Area (EEA)), region and geo-market names, cities with radius/unit, zip and custom-location samples, and location_types. The same fields appear under geo.excluded. Lists contain at most 25 items with full counts (country_count, country_group_count, region_count, city_count, geo_market_count, zip_count, custom_location_count, location_type_count); truncated: true on the containing object marks a capped list. Additional geo dimensions are retained. flexible_spec preserves each detailed-targeting group with capped interest, behavior and demographic names, per-list counts, flexible_spec_count and truncation flags. Custom audience lists are capped and counted too.
Requested compare fields are retained, including missing native results and cost_per_result. Friendly formulas are evaluated independently for each period. Native result values match by indicator and attribution window, preserving their expanded column paths. A field without comparable numeric values retains its current value and is listed in COMPARE_FIELD_UNSUPPORTED.
The cell estimate uses min(limit, row upper bound): account level without breakdowns or time buckets is one row per account. Time buckets and known bounded breakdowns increase that bound; unknown breakdown sizes and entity levels use the page limit. The actual output still cannot exceed 2,000 cells.
Only entities on the current page are fetched, including parents at lower reporting levels. For example, budget at ad level supplies parent campaign and ad set budgets. Account-level pages have no child IDs to enrich. Metadata has a separate 10-minute cache, so fresh refreshes performance while metadata keeps its own lifetime. Failed metadata reads return null entities and ENTITY_METADATA_UNAVAILABLE without failing performance rows; throttles still apply to subsequent uncached reads.
Multiple Accounts
Send accountIds with 2 to 5 distinct account IDs instead of the X-Ad-Account header. Supplying both returns 400 ACCOUNT_SCOPE_CONFLICT. The CLI uses --accounts act_123,act_456; MCP uses accountIds instead of accountId. Numeric IDs are accepted and normalized to act_ IDs for metadata and cursor-map keys.
{
"accountIds": ["act_123", "act_456"],
"fields": ["spend", "purchases"],
"datePreset": "last_7d",
"compare": { "type": "previous_period" },
"limit": 10
}
Accounts run sequentially through their own ownership check, currency, timezone, cache and throttle gates. Rows lead with account_id (Meta's numeric ID string) and account_currency. meta.accounts records each account's ID, currency, timezone, row count, cache, warnings, paging and optional safe error. Comparisons run per account, with ranges in each account's metadata. No totals are produced. Never add totals across accounts, even when currencies match; different currencies add MIXED_CURRENCIES.
Paging returns paging: { hasMore, byAccount: { ... } }. Continue with an after object mapping canonical account IDs to cursors. Use null for an exhausted account; omit a key only to start its first page:
{ "after": { "act_123": "CURSOR_FROM_PREVIOUS_PAGE", "act_456": null } }
Keep the remaining query options unchanged. CLI example: --after '{"act_123":"CURSOR_FROM_PREVIOUS_PAGE","act_456":null}'. limit applies per account, within the combined 2,000-cell bound.
Some account failures return 200 with successful rows, PARTIAL_RESULTS, and failed accounts' safe error: { code, retryAfterSeconds? }. Retry failed accounts separately after the stated wait. If every account fails, the request fails, preferring 429 if any account was throttled. Multi-account requests are sync only; explicit async or a required async fallback returns 400 MULTI_ACCOUNT_SYNC_ONLY. Narrow the request or query accounts separately.
History
These read-only endpoints use your API key and the same account access as the History page. The History page is open to everyone; reading History through the API, CLI and MCP is in beta. They do not require a current Facebook connection and are limited to 60 requests per minute per user, per endpoint.
| Endpoint | Result |
|---|---|
GET /api/v1/uploader-history | Ads Uploader launches and changes, newest first, as compact History rows |
GET /api/v1/uploader-history/launches/{launchId} | One launch with campaigns, ad sets, ads, results and failures |
Supply account_id or the X-Ad-Account header for both. The list also accepts days (7, 30 or 90; default 30), type (all, launches, changes; edits is also accepted), source (web, duplicator, mcp, cli, api), member (a team member ID), q (up to 200 characters), limit (1–100, default 50) and before. Pass paging.before to continue with the same filters. The response contains account, team, pendingSuggestions, rows and paging, plus members for a team. Change rows contain one setting's readable before/after values, status and edit ID; launch rows contain the ad count, campaign, ad set, outcome and launch ID.
Use ads history / ads history launch in the CLI or ads_list_history / ads_get_launch in MCP. GET /api/v1/history remains Meta's activity log, including edits made in Ads Manager and other tools; its CLI command is ads meta-activity and its MCP tool is ads_list_change_history.
Resource Reads
Use health for “how is this campaign doing”, Ads Uploader History for launches and changes, Meta activity for Ads Manager changes, and estimates for “how big is this audience”. Use Insights queries for custom performance analysis. New resource reads use the same connected-account checks and beta access as reporting. Existing campaign, ad set and ad lists keep their existing access and concurrency behavior.
REST endpoint under /api/v1 | CLI | MCP | Cache |
|---|---|---|---|
GET /campaigns, /campaigns/:id/adsets, /adsets/:id/ads | campaigns, adsets, adset | Existing list tools | Always fresh |
GET /insights/health | insights:health | ads_campaign_health | Insights TTL; entity context 10 minutes |
GET /audiences | audiences | ads_list_audiences | 10 minutes |
GET /conversions | conversions | ads_list_conversions | 1 hour |
POST /estimates | estimate | ads_estimate_delivery | 10 minutes |
GET /history | meta-activity | ads_list_change_history | 5 minutes |
GET /library/images, /library/videos | library:images, library:videos | ads_list_library_media | 10 minutes |
GET /pages/:pageId/posts, /instagram/:igUserId/posts | posts | ads_list_posts | 5 minutes |
GET /accounts/:accountId | account:info <id> | ads_get_account | 5 minutes |
Send the usual bearer token and select the account with X-Ad-Account or the account_id query parameter (including estimates); account details use the path ID and still check ownership. Each operation allows 20 requests per minute. New resource lists default to 25 rows (maximum 100). Existing campaign and parent-scoped ad-set/ad lists retain the 100-row default and 500-row cap. Lists include paging.after and paging.hasMore. Health is capped at 25. Keep filters unchanged when passing after. Responses include meta.cache; use fresh=true or CLI --fresh to bypass the cache. Resource caches are scoped to the user and account.
Existing list endpoints ignore unknown query parameters, use the first occurrence of repeated legacy parameters, and accept long search strings without trimming them. Exact status=active filters active delivery states; other status strings mean no filter. New parameters such as fresh, after and ad-list include are validated. New resource endpoints remain strict about unknown and duplicate parameters.
Campaign, ad set and ad lists retain id and name, adding status and setup details. Campaign and ad set budgets use major units with dailyBudgetMinor, lifetimeBudgetMinor and other raw minor strings. Account details and estimate curves use <field>_minor. Unknown currency precision produces null major amounts while retaining the raw string. Ads include creative IDs and preview links. Name searches run on Meta.
Campaign Health
ads insights:health --campaign 123 --preset last_30d --json
ads insights:health --since 2026-08-01 --until 2026-08-07 --limit 10
REST accepts campaignId, datePreset or a JSON-encoded timeRange object. Without an ID, it reads campaigns with delivery in the range and adds notDelivering, a campaign summary of up to 50 current WITH_ISSUES or DISAPPROVED ads, plus up to 50 ACTIVE ads in campaigns with no delivery in that range. notDeliveringCoverage reports the limit and whether more ads exist for each sample; its activeNoDelivery read is skipped with skipped: "more_delivering_campaigns" when more delivering campaign pages remain. A failed optional read adds HEALTH_NOT_DELIVERING_UNAVAILABLE or HEALTH_ACTIVE_NO_DELIVERY_UNAVAILABLE and leaves that sample’s coverage unknown while preserving the other sample. Each campaign includes spend, impressions, reach, frequency, link clicks, link CTR, CPM, current status, objective, budget, issues and learning-stage counts. The response preserves native results, cost_per_result and result_rate as separate indicator/value/window arrays with readable labels. It never labels total actions as conversions. No delivery rows means null performance metrics, not zero.
The issue and learning summary reads at most 100 ad sets per campaign. adsetCoverage and warnings identify partial or unavailable metadata. deliveryState combines current status with delivery observed in the selected range; it does not promise that the campaign is delivering right now. Performance and entity caches have independent lifetimes; health fresh refreshes both.
Definitions, Meta Activity, Library and Posts
Audiences support type=custom|lookalike|saved|all and search. Lookalikes include only origin IDs, country and ratio from their specification; saved audiences include a targeting summary. For all, one cursor walks custom/lookalike audiences then saved audiences. Conversion definitions use a cursor across pixels then custom conversions, plus a static standardEvents list. The limit bounds both definition lists together. Custom conversion names share the Insights discovery cache. Rules are trimmed to 1,000 characters.
GET /history reads Meta's account activity log, including edits in Ads Manager and other tools. Use GET /uploader-history for launches and changes made through Ads Uploader. Use since and until as inclusive UTC dates, with an optional numeric objectId. The default is the last 7 days; the maximum span is 90 days. extra_data is parsed when valid JSON and trimmed to 1 KB. History IDs are strings, including nested extra_data IDs. Integer literals of 16 or more digits in Meta history JSON are preserved exactly as strings before parsing; secret redaction and the 1 KB extra-data bound still apply.
The creative library contains media already in the Meta ad account, separate from Ads Uploader upload batches. Page posts require an accessible Page with advertising or management tasks; connected Instagram media requires the corresponding granted scopes. An inaccessible identity returns 404. Very large connected-Page lists can reach the bounded authorization lookup and return 422. Page messages and Instagram captions are trimmed to 500 characters. Page access tokens are never returned.
Estimates and Account Details
{
"targeting": { "countries": ["US"], "ageMin": 21, "genders": "women" },
"optimizationGoal": "REACH"
}
POST this body to /estimates, or save the targeting object alone for ads estimate --targeting targeting.json --goal REACH. Estimates use the ad-creation targeting normalizer and builder, including its selection flags for age, gender and detailed targeting. At least one location is required. Estimates also accept customAudiences and excludedCustomAudiences as numeric ID strings. Supply promotedObject when needed by the goal. The response contains audience bounds, daily active users, readiness and at most 20 sampled curve points, including the endpoints. Curve actions are predictions, not reported conversions.
ads account:info act_123 reads status and disable-reason labels, currency, timezone, spend cap, amount spent, balance, minimum daily budget, business and funding display. Funding details contain only Meta's type and display string. Select your CLI default account with ads account act_123.
Recommendations and opportunity-score reads are not currently exposed.
Reporting Beta and Safety Limits
Reporting and edits are available to beta accounts while in beta. Existing CLI/MCP access is still required. Hosted MCP hides all reporting and edit tools for accounts without access; local MCP keeps them listed and the server enforces access. Existing campaign, ad set and ad lists remain available. The CLI/API static field catalog stays accessible; account-scoped discovery requires beta access. Access errors display the server's explanation.
Meta's reported API usage determines when fresh calls pause. At 75% usage, Ads Uploader waits for Meta's recovery estimate; without an estimate, the wait is 60 seconds, or 300 seconds at 90% usage. Fresh calls also share a concurrency cap of 5 across the app and 1 per account for each read family. A call waits up to 20 seconds in total for app and account capacity, as described under Rate Limits.
Ads Uploader also has a short-window read safety limit: a burst of 120 calls per user, replenished at 30 calls per minute, and 240 calls per ad account across users, replenished at 60 calls per minute. This is an Ads Uploader limit, not a Meta limit. Async status checks skip concurrency slots but use this safety limit. Existing campaign, ad set and ad lists keep their request burst limit and now respect Meta usage signals. Cached results make no Meta calls.
Edits, previews, applies and copies skip every Ads Uploader read limit, pause and budget, and go straight to Meta; only one apply runs at a time per ad account. They stop only when Meta itself throttles a request (Meta error 4, 17, 32, 613 or 80000-80014, or HTTP 429). That stop says it was Meta, with Meta's code and Meta's own wait, and carries source: "meta", metaCode and metaSubcode; untouched changes stay pending.
Meta utilization at 75% triggers backoff for the relevant app or account bucket. The default wait is 60 seconds below 90% and 300 seconds at or above 90%; Meta's recovery estimate takes precedence for its business-use-case bucket. Management backoff leaves performance reads available unless an app or insights backoff also applies. Cached results remain available. Fresh entity includes and prior-period comparisons can be deferred, preserving current rows with ENTITY_METADATA_DEFERRED or COMPARE_DEFERRED.
| Error code | Meaning |
|---|---|
META_READS_DISABLED (403) | Reporting is temporarily disabled |
META_AGENT_NOT_ENABLED, META_READS_NOT_ENABLED or META_EDITS_NOT_ENABLED (403) | This feature is available to beta accounts while in beta |
META_READS_BUSY (429) | App concurrency is full; retry after 5 seconds |
META_READS_BACKING_OFF (429) | Wait for the remaining Meta recovery interval |
READS_GUARD_LIMIT (429) | Ads Uploader short-window safety limit reached for your user or ad account; wait for Retry-After |
META_READS_HOURLY_LIMIT, META_READS_ACCOUNT_LIMIT (429) | Legacy aliases replaced by READS_GUARD_LIMIT; these describe Ads Uploader safety limits, not Meta limits |
Use the response's Retry-After value for every 429.
Campaign health adds result: { source, metric, label, value, costPerResult }. Its source is meta_native, derived_from_optimization_goal, mixed or unknown. Native arrays remain intact. If native results are empty, the service reuses the bounded setup read and reads at most 100 ad set performance rows for the same date range. Only ad sets with impressions or spend in that range determine the fallback. Matching metrics can be combined; mixed metrics are returned under result.adsets and never totalled. VALUE also returns purchase_value. Reach and estimated recall use campaign-grain values. Missing setup, delivery data or incomplete pages leave the result unknown. metricNotes explains the derivation, and CLI health tables show result value and cost.
learningStageCounts contains counts by returned stage, or an empty object when no stage was reported. It is null only when ad sets could not be read. The prior learningStages field remains an alias. Account spend_cap: null, spend_cap_minor: "0" and hasSpendCap: false means no cap; CLI shows None. All major-unit entity, account and enriched-list money values are numbers, while raw minor-unit values stay strings.