Skip to main content
Meta OAuthException: Telling an Expired Token From a Missing Scope
integrationsmeta-graph-apierror-handling+4

Meta OAuthException: Telling an Expired Token From a Missing Scope

Meta's Graph API returns type OAuthException for both an expired access token and a missing permission scope — two failures with opposite fixes. How to tell them apart using the numeric codes Meta documents, and why classifying on the type string alone leaves users in a loop.

The same error type, two opposite remedies

Meta’s Graph API returns type: 'OAuthException' when an access token has expired. It returns the same type: 'OAuthException' when the account is simply missing a permission scope — code 200 (“Permissions error”), code 10 (“Application does not have permission”).

Same outer envelope. Opposite fixes. An expired token is fixed by reconnecting. A missing scope is not: reconnecting requests exactly the same scopes again, so the user ends up in a loop — click Reconnect, run the OAuth flow, send, fail, repeat. The button can never resolve the problem it is being offered for.

We met this seam in our own Instagram integration, which is why we can be specific about where it sits. A check keyed on the type string alone — the obvious reading of Meta’s error documentation, and what most integration guides show — cannot separate the two cases. The numeric codes can.

Why a type-based check cannot work here

An isAuthError helper that tests the broad type string treats every OAuthException as a dead token. Downstream, that flips the channel connection state to token_expired and surfaces a Reconnect call-to-action. For a genuinely expired token that is exactly right. For a missing scope it is a dead end that hides the real cause.

The visible symptom is only a wrong error message. The consequence is larger. A user in that state cannot self-serve, cannot escalate usefully (“I already reconnected, twice”), and the operator on the other end has no signal that the problem is permissions rather than authentication. Everyone involved is then working from a diagnosis the API never actually made.

Two changes that separate the cases

Pin the dead-token check to the codes Meta documents for that failure mode — 190, 102, 463, and HTTP 401. Anything else arriving as an OAuthException is then no longer able to masquerade as a token problem, and a permissions failure surfaces as a permissions failure.

Log Meta’s full response — code, subcode, type, message — centrally, before any caller reduces it to a coarse exception. This is the half worth keeping even where the first change is not needed. A misclassification is one line to correct; it is hard to diagnose, because by the time anyone looks, the vendor’s actual code has already been discarded.

The transferable bit for anyone integrating a third-party API

Meta is not unusual here. Stripe, Google, Slack, Shopify — most large APIs reuse a broad error envelope across semantically distinct failure modes, because that envelope was designed for the machine reading the HTTP status, not for the human reading a stack trace three days later.

If you classify errors by the outer type alone, you will eventually conflate two failures that need opposite fixes. Here it is “token is dead” versus “permission was never granted.” Yours might be rate-limit versus quota-exhausted, or transient network versus permanently-rejected-payload. The pattern is the same: the vendor tells you specifically what happened in a numeric code, and that information is thrown away the first time the error is wrapped.

Two rules we now apply on every third-party integration we ship:

  1. Pin error classification to the specific numeric codes the vendor documents for each failure mode. Never to the outer type alone.
  2. Log the full vendor payload centrally before any coarsening step. If the raw response never reaches your logs, the reason a user is stuck is gone the moment it happens.

Neither is glamorous. Both are cheap, and both are the difference between a user who can be helped and a user staring at a button that will never do anything.

Have a project in mind?

Fixed price after a paid discovery — no hourly billing. A real engineer reads every enquiry, and we reply within 24 hours.