The Meta Ad Library API, Field by Field
Every field, which ads populate it, the token path that blocks most people, and the security detail in ad_snapshot_url that will leak your access token.
12 min read · 20 Aug 2026
Most people who set out to use the Meta Ad Library API lose their first day to the access token and their second to discovering that the field they are building the product around is null for every ad they care about.
This page is the reference we wanted while building against it: the token path in order, every field with the ads that actually populate it, what the search parameters match, and the handful of behaviours that are not obvious from Meta's documentation.
Provenance, stated up front. The field and parameter tables come from Meta's published references — the ads_archive edge and the ArchivedAd node — checked on 20 August 2026. Meta changes those pages, so check the version you are building against. Where a line is a decision we made writing a client rather than something Meta documents, it says so. Our four endpoints on this API are still waiting on an access token, so nothing here is a measurement against live traffic.
What the archive actually contains
Two populations of ads with completely different levels of detail, and confusing them is the single most common way a project's assumptions break.
Ads about social issues, elections or politics. Kept for seven years, visible whether active or inactive, and carrying the fields with real analytical value: spend ranges, impression ranges, funding bylines, demographic distribution, estimated audience size.
All other ads. Visible while active, and archived for one year after their last impression. No spend, no impressions, no demographics.
The retention asymmetry has an operational consequence people discover late: for commercial ads, an ad that stops running stops being reachable in the general case. If your product needs the history of what a US brand ran last quarter, the archive is not going to give it to you retroactively — you have to have been collecting while the ads were live. Design your ingestion around that, or you are building a product on a window rather than an archive.
A third split, orthogonal to the first, is geographic. Several fields are documented as available only for ads delivered in the EU or UK, regardless of whether the ad is political. If your market is the US, those fields are permanently null for you. The risk and coverage argument for this whole data category goes into why the geography matters more than it looks.
Getting a token
This is the wall. The steps, in order:
- Confirm your identity and location. Meta routes this through the same process required to run ads about social issues, elections or politics — government ID and proof of residence. Meta's own guidance says it can take a few days; third-party accounts of the process report roughly two to seven business days.
- Create a Meta for Developers account and accept the Platform Policy.
- Create an app. Business type. Add the Ad Library API as a product on the app.
- Generate a user access token with
ads_read, most easily through the Graph API Explorer in the developer dashboard. The Ad Library API reads with a user token tied to your verified account, not an app token. - Extend the token. Explorer tokens are short-lived — on the order of an hour or two. Long-lived user tokens run about 60 days.
Three things worth knowing before you start:
Identity confirmation is a person-level gate, not a company-level one. It is tied to the human being whose ID was submitted. If that person leaves, plan for it.
Sixty days is not "set and forget." A long-lived user token expires, which means a scheduled ingestion pipeline will stop working roughly two months after launch, on a day nobody remembers. Put the expiry date in your calendar, alert on the token-invalid error class specifically, and make it operationally distinct from an outage — because it will look like one.
A rejected token does not look like a rejected token. More on that under errors.
Query parameters
Documented on the ads_archive edge, checked 20 August 2026.
| Parameter | Required | Values / notes |
|---|---|---|
ad_reached_countries |
Yes | ALL or ISO country codes. The one genuinely mandatory parameter |
search_terms |
No | Max 100 characters. A blank space between words is a logical AND |
search_type |
No | KEYWORD_UNORDERED (default) or KEYWORD_EXACT_PHRASE |
search_page_ids |
No | Up to 10 Facebook Page IDs |
ad_type |
No | ALL (default), POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, EMPLOYMENT_ADS, FINANCIAL_PRODUCTS_AND_SERVICES_ADS |
ad_active_status |
No | ACTIVE (default), INACTIVE, ALL |
ad_delivery_date_min / _max |
No | YYYY-MM-DD |
media_type |
No | ALL, IMAGE, MEME, VIDEO, NONE |
publisher_platforms |
No | FACEBOOK, INSTAGRAM, AUDIENCE_NETWORK, MESSENGER, WHATSAPP, OCULUS, THREADS |
languages |
No | ISO 639-1 / 639-3 codes |
bylines |
No | Political ads only. Complete byline text |
delivery_by_region |
No | Political ads only |
estimated_audience_size_min / _max |
No | Political ads only |
unmask_removed_content |
No | Boolean, default false |
Two notes that will save you a confused afternoon:
ad_active_status defaults to ACTIVE. If you write a query, get results, and later wonder why history is missing, this is why. Set it to ALL explicitly when you mean all.
ad_type values move. The financial category is documented as FINANCIAL_PRODUCTS_AND_SERVICES_ADS; older clients and older blog posts use CREDIT_ADS. If you are copying a snippet from anywhere, check this enum against the current reference rather than trusting it.
What the search actually matches
search_terms is matched against ad text and advertiser name, and the semantics are more literal than people expect:
KEYWORD_UNORDEREDtreats a space as AND.nike runningfinds ads containing both words, in any order, not the phrase.KEYWORD_EXACT_PHRASEtreats the string as one phrase. Separate multiple phrases with commas.- Keywords are not translated. Searching
shoeswill not surface an ad written in German. If you want European coverage you supply the terms in the target language, per language. This is the single most common reason a search "misses" ads that are obviously there.
Every field, and what populates it
The ArchivedAd node, with availability marked. This is the table to check before you promise a field to anyone.
Always available
| Field | Type | Notes |
|---|---|---|
id |
numeric string | The Library ID. This is what ?id= in an Ad Library URL refers to |
page_id |
numeric string | The Page that ran the ad — the join key for everything |
page_name |
string | Display name of that Page |
ad_creation_time |
string | UTC, e.g. 2019-01-24T19:02:04+0000 |
ad_delivery_start_time |
string | When the advertiser wanted delivery to start |
ad_delivery_stop_time |
string | Blank means still running, not unknown |
ad_creative_bodies |
list<string> | Body text, one entry per unique ad card |
ad_creative_link_titles |
list<string> | Headline per card |
ad_creative_link_captions |
list<string> | The caption in the CTA section — usually a display domain, not a URL |
ad_creative_link_descriptions |
list<string> | Description text per card |
ad_snapshot_url |
string | Renders the archived ad. See the security note below |
publisher_platforms |
list<enum> | Where it appeared |
languages |
list<string> | ISO 639-1 |
total_reach_by_location |
list<KeyValue> | Reach by location, for EU, UK and Brazil |
The lists are per card, not per ad: a carousel produces multiple entries in ad_creative_bodies, index-aligned across the four creative fields. If you take [0] and move on — as we do in our own normalised schema — you are keeping the first card and discarding the rest. That is a defensible simplification and you should know you are making it.
Political and issue ads only
| Field | Notes |
|---|---|
spend |
A range object with lower_bound / upper_bound, not a number |
impressions |
Also a range |
currency |
ISO code for the spend range |
bylines |
Who funded it — the "Paid for by" string |
estimated_audience_size |
Range; how many accounts met the targeting criteria |
demographic_distribution |
Age and gender of accounts reached |
delivery_by_region |
Sub-country regional distribution |
br_total_reach |
Brazil political/issue ads specifically |
EU / UK only
| Field | Notes |
|---|---|
eu_total_reach |
Combined reach across EU locations |
age_country_gender_reach_breakdown |
The richest demographic field in the API |
beneficiary_payers |
Reported beneficiary and payer — an EU transparency requirement |
target_ages |
Documented for EU/UK, and for Brazilian political ads |
target_gender |
Same availability |
target_locations |
Included and excluded locations. Same availability |
An engineering decision, not a documented rule: we request the political-only fields only when the query is for political ads, and the base field set otherwise. The reasoning is that a field projection that a query cannot satisfy risks failing the entire call, and a failed call is strictly worse than a null column. Meta documents the filter parameters as political-only; the field behaviour is something we chose to be conservative about. If you want to know whether requesting them broadly works for your app, test it against your own token rather than trusting either us or a blog post.
Notice what is missing from all three tables: there is no field for clicks, CTR, conversions, or the call-to-action button. There is no field containing the image or video file. What you get is what the advertiser said, where, and for how long.
The security detail in ad_snapshot_url
This one is worth the price of the page.
ad_snapshot_url is returned with your access token in the query string. It has to be — the URL renders a Meta-hosted page that requires authorisation, and Meta authorises it with the token you called the API with.
The consequences follow immediately:
- If you store the response as returned, your access token is now in your database.
- If you log the response, your token is in your logs.
- If you pass the ad object to a browser client, you have handed every user a working Meta access token.
- If you expose a raw passthrough of the upstream payload to your own API's customers, you have handed it to them.
Strip it. In our client the token is removed from ad_snapshot_url everywhere, including out of the raw passthrough, and where the URL cannot be parsed the field is dropped rather than passed through on the assumption that it is fine.
For a public, shareable, token-free permalink to any ad, construct it from the ID instead:
https://www.facebook.com/ads/library/?id={ad.id}
That link works for anyone, does not expire with your token, and is the one to put in a UI.
Pagination, rate limits, and errors
Pagination is standard Graph cursoring. Responses carry paging.cursors.after and paging.next; pass the cursor as after on the following call. Meta documents that reaching an empty data array means you are at the end — so treat "empty page" as termination rather than as an error, and check paging.next as well as the cursor before deciding there is more.
Rate limits. The documented ceiling is 200 calls per hour per user access token, and the throttling error is code 613 (Calls to this api have exceeded the rate limit). Third-party accounts report that requests chaining large field projections across many pages hit 613 well below the nominal count, on the theory that Meta throttles against an internal cost estimate rather than a raw call counter. We have not verified that ourselves — treat it as a reason to build backoff rather than as a number to design against. Back off, wait, resume; do not retry immediately.
Errors are where the Graph API is least helpful, and the practical advice is a mindset rather than a table: on this API, a failure is a configuration problem until proven otherwise. An expired token, an app without Ad Library access, incomplete identity confirmation, and a filter combination Meta does not permit all surface as an HTTP 400 or 403 rather than as anything self-describing. If your client translates those into "upstream unavailable" you will send whoever is on call to look at Meta's status page, which will be green, and they will lose an hour before checking the token.
Two error codes worth special-casing by name: 190 is an invalid or expired token, and 613 is throttling. Everything else, start with your own configuration.
The advertiser-lookup problem, and how to work around it
search_page_ids is the endpoint you actually want for competitor monitoring: give it a Page ID and get that advertiser's ads. The problem is obtaining the Page ID, because Meta publishes no advertiser-search edge. There is no "find the Page ID for this brand" endpoint in the Ad Library API.
Two ways through it:
Manually. Find the advertiser in the Ad Library web UI and take view_all_page_id out of the URL. Fine for ten advertisers, useless for a product.
Derive it. Search ads_archive for the brand name, then fold the results down to distinct page_id values, keeping a count of how many of the sampled ads each Page ran. That count is a rough but genuinely useful ordering — the Page that appears most often in a sample of ads matching "Nike" is very likely the one you want, and the long tail is affiliates, resellers and impersonators, which is itself informative. This stays entirely inside the official API and it is how we implemented advertiser lookup rather than reaching for the UI.
Related gap: reading one ad by its archive ID is not reliable. The archive is a search edge, not a fully addressable node collection, and a direct read of an ad ID is not guaranteed to resolve. The fallback that works is to search the advertiser's archive for the ID — which means you need the Page ID to fetch an ad you already have the ID for. Cap the scan; walking an unbounded archive looking for one ad is how you meet error 613.
Getting creative assets and transcripts out of an ad
The API does not give you the files. You get ad_snapshot_url, which renders the ad; the image, the video, the audio and any transcript of it require fetching and parsing that rendered page. At that moment you are HTML scraping a Meta property, with all the fragility that implies, and you have left the sanctioned path that made this data attractive.
It is a legitimate thing to build. Build it as a separate, isolated, expected-to-break component with its own alerting rather than inlining it into your API client, so that when it breaks the rest of the pipeline keeps working — the general pattern for surviving that kind of breakage applies here more than anywhere.
What we built, and where it stands
Four endpoints on this API: search the archive, all ads for one Page, one ad by ID with the archive-scan fallback, and the derived advertiser lookup described above. Official Graph API only, called from our own IP, no proxies, no anti-bot surface, marginal cost effectively zero.
They are not live. All four are blocked on FACEBOOK_ADLIB_ACCESS_TOKEN and return a clean not_configured error naming the missing variable — deliberately, rather than a generic 500, because a missing credential and an upstream outage need to send you to different places. When the token lands, they light up with no code change. We would rather publish that than imply a working integration we do not have.
Whether this route is right for you depends almost entirely on whether you need history for inactive commercial ads, creative files, or coverage beyond Meta — none of which this API provides. That trade-off, and the cases where a scraper or an aggregator is the better answer, is worked through here.
The short version
- Two populations: political ads get spend, impressions and demographics and seven years of retention; everything else gets creative text and dates, and vanishes a year after its last impression.
- Several of the best fields are EU/UK only. A US-focused product does not get them, ever.
- The token gate is identity confirmation plus
ads_read, and long-lived tokens expire in about 60 days — that is a scheduled outage, so alert on it. ad_reached_countriesis the only required parameter.ad_active_statusdefaults toACTIVEand will silently hide history.- Search treats spaces as AND and does not translate. Supply terms per language.
ad_snapshot_urlcontains your access token. Strip it before you store, log, or return it.- There is no advertiser search. Derive Page IDs by searching ads and folding to distinct pages.
- On a 400 or 403, suspect your configuration before you suspect Meta.