There are two ways a vendor can retire an API version. It can reject calls to the dead version, which is loud and annoying and gets fixed the same afternoon. Or it can keep serving those calls out of a newer version, which is polite, invisible, and much worse.
Meta does the second one. From Meta's Graph API versioning docs:
A version will no longer be usable two years after the date that the subsequent version is released.
and, immediately after:
For APIs, once a version is no longer usable, any calls made to it will be defaulted to the next oldest, usable version.
Graph API v20.0 is listed in the changelog as "Available Until September 24, 2026." It is the oldest version still available, and the only one that expires in 2026 — v21.0 runs until January 21, 2027, v22.0 until May 20, 2027.
So on September 24, requests to https://graph.facebook.com/v20.0/... do not start returning 400. They start getting answered by v21 code. The path in your request still reads v20.0. Your dashboards still show 200s. Nothing in the response announces that the switch happened.
The failure modes split into two groups, and only one of them pages you
This is the part worth internalizing before September, because it determines what your incident review will miss.
Everything that changed between v20 and v21 lands on your integration at the same instant, from the same cause. But it doesn't land the same way.
The loud group — you'll find these in your error logs within a day:
-
POST /{app_id}/page_activitiesis gone. The Messaging Events API is, per the v21 changelog, "no longer supported in any future releases of Graph API, starting with version 21.0." Calls 404. -
Removed Insights metrics return real errors. Request
video_viewson IG media insights and you get(#100) Starting from version 21+, the following metric is no longer supported: video_views. That's an explicit, greppable error. -
Legacy campaign objectives stop being creatable. On v21 you can't create new ad sets or ads with non-ODAX objectives via
POST /{ad_account_id}/campaigns,/adsets, or/ads. Creation fails. -
Image Expansion moved. It's now part of Standard Enhancements and has to be set via
standard_enhancementsincreative_features_spec.
Fix these and it feels like you've handled the migration. You have not.
The quiet group is everything that changed shape or value without changing status code. And the six Instagram User Insights time-series metrics removed in v21 — profile_views, website_clicks, email_contacts, get_direction_clicks, text_message_clicks, phone_call_clicks — are the ones to watch, because of how they're usually consumed rather than how the API responds.
The API is loud. Your pipeline is quiet.
Here's the trap, and it's a pipeline problem more than an API problem.
Insights endpoints take a comma-separated metric list. A nightly ETL that pulls twelve metrics per account across four hundred accounts does not, as a rule, fail the whole run when Meta rejects one of them. It does something like this:
for metric in METRICS:
try:
rows = fetch_insights(account_id, metric, since, until)
write(rows)
except GraphAPIError as e:
log.warning("skipping %s for %s: %s", metric, account_id, e)
continue
That code is not wrong. It's the code you write after the third time one bad account killed an eight-hour backfill. But on September 24 it converts an explicit (#100) error into a WARNING line in a log nobody reads, and the dashboard downstream renders a metric that simply stops having data — not a gap with an error banner, just a line that goes flat and a "profile views" tile that reads 0.
Zero is a plausible number. That's the whole problem. Nobody files a ticket about a chart that shows a small number; they file tickets about charts that show an error. Reporting built on this quietly becomes wrong on a Thursday and gets discovered in a quarterly review, if at all.
The same shape applies to any per-field try/except, any IGNORE_MALFORMED-style ingest setting, any connector that treats a partial response as a successful sync. If you run Airbyte, Fivetran, Supermetrics, or a homegrown equivalent against Meta, the question isn't "does the API error" — it's "what does my connector do with a metric-level error inside an otherwise-successful call."
Grep won't find all of it
The obvious audit is to search for v20.0 across your codebase. Do that first, but know that it's the easy half. The version pin also lives in places that don't contain that string:
- App-level default version. Calls made without an explicit version in the path resolve to a version configured for your app, not one written in your code. Open the App Dashboard and check what your app is actually set to before you conclude you're not affected.
-
SDK pins. Meta is explicit that this is a landmine: "For SDKs, a version will always remain available as it is a downloadable package. However, the SDK may rely upon APIs or methods which no longer work, so you should assume an end-of-life SDK is no longer functional." A pinned
facebook-businessSDK from 2024 is a v20 client wearing a version number that never expires. - Connector configuration, not code. Airbyte/Fivetran/Supermetrics/Zapier/n8n Meta connectors store an API version in saved config. It's in a database row, not your repo.
- Mobile SDKs shipped to devices. You don't control the upgrade cadence of an app someone installed in 2024.
- Saved webhook subscriptions, which were created against a version at subscription time.
The useful part: you can diff this today
The thing that makes this different from most deprecation posts is that the "after" state already exists. v21 through v25 are live right now. You do not have to wait for September 24 to find out what changes — you can find out this week, with seven weeks left to act.
The check that actually pays:
- Capture your real query set. Not a representative sample you wrote by hand — the actual distinct (endpoint, fields, metrics) tuples your integration issues in a week. Pull them from access logs or add a one-line logger.
-
Replay each one against
/v20.0/and/v21.0/back to back, same token, same params, same time window. -
Diff on shape and value, not status. Both will mostly return 200. What you're looking for is: keys present in one and absent in the other,
dataarrays that come back shorter, numbers that moved more than rounding explains, and enum values in the v21 response that your code doesn't have a branch for. -
Run the same replay through your pipeline, not just curl. The
(#100)errors are only interesting in terms of what your ingest code does with them. Point a staging connector at v21 and see what lands in the warehouse.
Step 4 is the one people skip and it's the one that matters, because as established, the API's honesty is not the issue. Your error handling is.
Why this pattern keeps showing up
Fall-forward versioning — serving expired versions out of newer code instead of rejecting them — is now the default posture at Meta, Klaviyo, monday.com, Contentful, and Xero. The vendor logic is sound: nobody's integration goes dark on a Tuesday, and the long tail of unmaintained clients keeps limping along.
The cost is borne somewhere else. It converts an availability failure, which your monitoring is built to catch, into a correctness failure, which it usually isn't. A version string in a URL stops being a contract and becomes a preference the server honors right up until the day it doesn't — while still echoing your preference back to you.
The defense isn't to stop pinning versions. It's to treat every pinned version in your stack as a dated liability with a known expiry, and to assert on the shape of what comes back instead of trusting that a 200 means the contract held.
For Meta specifically, the date is September 24, 2026, and the diff is available today.
I write about API drift — the changes that keep returning 200 while quietly changing what your integration receives. FlareCanary monitors API responses and schemas for exactly this class of change.
Top comments (0)