You can pull Facebook Page analytics two ways: export a report from Meta Business Suite by hand, or call the GET /{page-id}/insights edge on the Graph API with a Page access token. The API is the only option that scales, and it is also the one that breaks when Meta retires a metric, which it has done repeatedly. This guide covers the token you need, the parameters that actually matter, the metrics that still work in 2026, and a Python function that pulls page level numbers into rows you can load anywhere.
read_insights and pages_read_engagement permissions, then call GET https://graph.facebook.com/v25.0/{page-id}/insights?metric=page_views_total&period=day&since=...&until=...&access_token=.... You can span at most 90 days per call with since and until, and Meta keeps only the last two years of Page Insights data.The awkward part of this API is not authentication. It is metric churn. Meta has spent the last two years replacing its impression and reach vocabulary with a views and viewers vocabulary, and every one of those changes has broken somebody’s dashboard. So this guide treats metric names as a moving target and shows you how to discover which ones your Page actually returns rather than hardcoding a list you copied from a tutorial.
Meta Business Suite export versus the Graph API
Meta Business Suite is fine for a one time look. You open the Insights area for the Page, pick a date range, and download the numbers. It is fast, it needs no code, and the metric labels are written in plain language rather than API identifiers. The catch is that the labels in the interface do not map one to one onto API metric names, so a number you read in the interface will not always match a number you compute from the API, and you cannot schedule the export.
The Graph API is the right tool when you need a nightly job, a warehouse table, or numbers for more than one Page. It gives you stable identifiers, explicit periods, and pagination. It also gives you the retirement notices, because the changelog is where Meta announces what is going away. If you already have a Facebook app wired up, most of the work is done. See our walkthrough on Facebook API authentication and token handling for the part that trips people up first.
What you need before the first call
Four things have to line up, and if any one is missing you get an error that does not name the real problem.
| Requirement | What it means |
|---|---|
| Page access token | Not a user token. Exchange the user token for a Page token via GET /me/accounts. |
| ANALYZE task | The person who granted the token must hold the ANALYZE task on that Page. |
| Permissions | read_insights and pages_read_engagement, both granted during login. |
| Audience size | Meta documents a 100 like minimum for Page Insights, and demographic breakdowns need at least 100 people in a segment. |
That last row is the reason a brand new Page returns empty arrays for everything. There is no error. The data array simply has no values in it, which reads like a bug and is not one.
The shape of a request
Every call is the same edge with a different parameter set. Here is the minimal version with curl.
curl -G "https://graph.facebook.com/v25.0/$PAGE_ID/insights" \
-d "metric=page_views_total,page_post_engagements" \
-d "period=day" \
-d "since=2026-08-01" \
-d "until=2026-08-31" \
-d "access_token=$PAGE_TOKEN"The parameters worth knowing are few, and the documented limits on them are the source of most confusion.
| Parameter | Notes |
|---|---|
metric | Required. Comma separated list. Omit it and the API returns error code 3001. |
period | One of day, week, days_28, month, lifetime, total_over_range. |
since and until | Meta documents a 90 day maximum span per call. The since date is included in the first value returned. |
date_preset | Shorthand ranges such as yesterday, last_7d, last_28d, last_90d. |
breakdown | Splits a metric by a dimension. Valid dimensions depend on the metric, and zero rows are dropped. |
Metric names change, so discover them
Meta has been retiring the impression and reach family in stages. Its developer blog announced that the impressions metric would be replaced by a views metric across API versions, and that Page fan metrics were being deprecated alongside the move to the new Pages experience. The Graph API v25.0 changelog, published in February 2026, went further and introduced viewer metrics such as page_total_media_view_unique and post_total_media_view_unique, describing the viewers metric as the intended replacement for the legacy reach metric. That changelog also lists a set of legacy impression, reach and video viewer metrics slated for retirement.
The practical response is to stop hardcoding metric lists. Request one metric per call while you are exploring, and let the failures tell you what is gone.
import os, requests
GRAPH = "https://graph.facebook.com/v25.0"
def probe(page_id, token, candidates):
"""Return the subset of candidate metrics this Page still answers."""
live = []
for name in candidates:
r = requests.get(
f"{GRAPH}/{page_id}/insights",
params={"metric": name, "period": "day",
"date_preset": "last_7d", "access_token": token},
timeout=30,
)
if r.status_code == 200 and r.json().get("data"):
live.append(name)
return livePulling page metrics into rows
Insights responses nest two levels deep. Each entry in data is one metric, and each entry carries a values array with one object per bucket. Flatten it before you do anything else, and follow paging.next until it disappears.
import os, requests
GRAPH = "https://graph.facebook.com/v25.0"
PAGE_ID = os.environ["FB_PAGE_ID"]
TOKEN = os.environ["FB_PAGE_TOKEN"]
def fetch_insights(metrics, period="day", since=None, until=None):
params = {
"metric": ",".join(metrics),
"period": period,
"access_token": TOKEN,
}
if since:
params["since"] = since
if until:
params["until"] = until
url = f"{GRAPH}/{PAGE_ID}/insights"
rows = []
while url:
resp = requests.get(url, params=params, timeout=30)
resp.raise_for_status()
payload = resp.json()
for entry in payload.get("data", []):
for bucket in entry.get("values", []):
rows.append({
"metric": entry["name"],
"period": entry["period"],
"end_time": bucket.get("end_time"),
"value": bucket.get("value"),
})
url = payload.get("paging", {}).get("next")
params = None # the next URL already carries every parameter
return rows
if __name__ == "__main__":
for row in fetch_insights(["page_views_total"], since="2026-08-01",
until="2026-08-31"):
print(row["end_time"], row["metric"], row["value"])Setting params = None after the first request matters. The cursor URL Meta hands back already contains the access token and every filter, so passing your original dictionary again can silently reset the window and put you in an endless loop.
Breakdowns, and where the value goes strange
Most page level metrics return a scalar in value. Some return an object instead, keyed by the breakdown dimension, so a country breakdown gives you a dictionary of country codes to counts rather than a number. Code that assumes a scalar will throw the moment somebody adds a demographic metric to the list. Branch on the type rather than trusting the metric name, because Meta has changed the return shape of individual metrics before.
Pagination behaves differently too. Page level insights paginate by time, so a long window produces several pages of the same metric. Object valued metrics usually come back in a single page because the whole distribution fits in one response.
Troubleshooting
Error code 3001, no metric specified. The metric parameter is missing or empty. This also happens when a list comprehension that builds the metric string returns nothing because your filter excluded everything.
Invalid metric error on a name that used to work. The metric was retired. Check the Graph API changelog for the version you are calling and migrate to the replacement. Pinning to an older version buys time but not much, since retirements are usually announced as applying across versions.
Empty data array with a 200 status. Either the Page is under the documented 100 like threshold, the window you asked for predates the two year retention limit, or the metric genuinely has no activity. Test with date_preset=last_28d on a known busy metric to tell these apart.
Request times out with many metrics. Meta’s own guidance is to reduce the number of metrics per call. Split a twenty metric request into four calls of five and the timeouts usually stop.
Numbers do not match Meta Business Suite. Expect this. The interface aggregates on its own definitions and time zone, and most metrics only refresh about once every 24 hours. Compare trends, not single day totals. The same discipline applies when you reconcile ad numbers, which we cover in the guide to managing Facebook ads through the API.
Frequently asked questions
Do I need a Business verified app to read Page Insights?
For your own Pages in development mode, no. To read insights for Pages you do not administer, your app needs App Review for the relevant permissions and Business Verification. Plan for that review cycle early, because it gates production access and is not something you can rush at launch.
How far back can I pull historical data?
Meta documents that only the last two years of insights data is available. Within that, a single call using since and until can cover at most 90 days, so a full backfill means looping through consecutive 90 day windows and stitching the results together yourself.
What is the difference between period and date_preset?
The period parameter sets the aggregation bucket, meaning whether each number covers a day, a week, 28 days, a month, a lifetime total, or the whole requested range. The date_preset parameter sets the date range instead of naming explicit since and until values.
Why did my reach metric stop returning data?
Meta is replacing legacy impression and reach metrics with views and viewers metrics. The v25.0 changelog introduces page_total_media_view_unique and related viewer metrics and lists legacy reach and impression metrics for retirement, so migrate rather than waiting for the old name to come back.
Can I get post level numbers from the same edge?
No. Page level metrics come from /{page-id}/insights, post level metrics from /{post-id}/insights. The parameter grammar is identical, but the metric vocabularies are separate, so a metric valid on one edge will usually fail on the other.
The bottom line
Getting Page Insights out of the Graph API is straightforward once the token has the ANALYZE task behind it. Write a flattening function, respect the 90 day request window and the two year retention limit, and follow paging.next properly. That covers the mechanics for good.
The maintenance burden is metric names, not code. Subscribe to the changelog, keep the raw responses, and build a probe step that tells you which metrics your Page still answers before a scheduled job runs. Consult the official Page Insights reference and the Graph API changelog whenever a number looks wrong. If you are also wiring up client side tracking, our notes on using the Facebook SDKs and on downloading a copy of your Facebook data cover the neighboring pieces.
