Last verified: 12 September 2026 - Next review: 12 December 2026. Verified against Graph API v24.0. Meta changes this flow; we re-verify this page quarterly.
The short answer
Three rungs, in this order:
- A short-lived user access token from Facebook Login. Lives about one to two hours.
- Exchange it, server-side, for a long-lived user access token. Lives about 60 days.
- Call
/me/accountswith that long-lived user token to get a Page access token. Meta's wording: “Long-lived Page access token do not have an expiration date and only expire or are invalidated under certain conditions.”
The whole thing turns on step 2 happening before step 3. A Page token minted from a short-lived user token inherits the short lifetime, and the response gives you no hint which one you got. That one ordering mistake is why most Facebook integrations work in testing and die an hour after they ship.
Instagram has its own ladder, and since Meta split the Instagram Platform APIs there are two of them. They are covered below - including which one has a real refresh endpoint, because Facebook does not.
Why your token keeps dying
The thing you are actually looking at:
1{
2 "error": {
3 "message": "Error validating access token: Session has expired on Friday, 11-Sep-26 14:22:01 PDT.",
4 "type": "OAuthException",
5 "code": 190,
6 "error_subcode": 463,
7 "fbtrace_id": "A1bC2dE3fG4h"
8 }
9}Code 190 means “the token is the problem”. The error_subcode tells you which problem, and they are not interchangeable: 463 wants a new token, 460 wants the user back in front of a login dialog, 492 is not a token problem at all.
Subcode | What Meta means by it | What actually fixes it |
|---|---|---|
| App Not Installed. “The User has not logged into your app.” | Full re-auth. The user removed your app or never finished the flow. |
| User Checkpointed. Meta has locked the account pending an action. | Nothing you can do server-side. The user must log in at facebook.com and clear it. |
| Password Changed. | Full re-auth. Re-exchanging will not work - the session behind the token is gone. |
| Expired. “Login status or access token has expired, been revoked, or is otherwise invalid.” | Re-auth. If you are seeing this on a Page token, you built it from a short-lived user token. |
| Unconfirmed User. | The user must confirm their account with Meta. |
| Invalid Access Token. | Re-auth. |
| “User associated with the Page access token does not have an appropriate role on the Page.” | Not a token problem. Restore the Page role, then re-fetch the Page token. |
One detail worth putting in your error handling now: Meta returns HTTP 400 for an expired token, not 401. If you branch on status code alone, a permanently dead token looks exactly like a transient failure and your retry loop will hammer it forever. Branch on code and error_subcode.
Step 1: get a short-lived user access token
Send the user through the login dialog with the permissions you actually need. For publishing to a Page:
Permission | Why you need it |
|---|---|
| List the Pages the user manages. Without it /me/accounts returns nothing. |
| Read Page content and metadata. |
| Create, edit and delete posts as the Page. This is the one that lets you publish. |
| Required when the Pages are owned by a Business rather than by the user directly. |
| Only if you need post or Page metrics. |
1https://www.facebook.com/v24.0/dialog/oauth
2 ?client_id={app-id}
3 &redirect_uri={your-redirect-uri}
4 &state={csrf-token}
5 &scope=pages_show_list,pages_read_engagement,pages_manage_posts
6 &response_type=codeThen swap the code for a token:
1curl -G "https://graph.facebook.com/v24.0/oauth/access_token" \
2 -d "client_id=${APP_ID}" \
3 -d "redirect_uri=${REDIRECT_URI}" \
4 -d "client_secret=${APP_SECRET}" \
5 -d "code=${CODE}"Two traps before you go further.
- Graph API Explorer tokens are for testing only. They are short-lived, scoped to your own account, and they will be revoked out from under you. Use the Explorer to check a response shape, never to configure a deployment.
- Every one of those permissions needs App Review. While your app is in development mode the flow works perfectly for anyone with an admin, developer or tester role, and then fails for real users. This is a very common way to ship a broken integration with a green test suite.
Step 2: exchange it for a long-lived user token
1curl -G "https://graph.facebook.com/v24.0/oauth/access_token" \
2 -d "grant_type=fb_exchange_token" \
3 -d "client_id=${APP_ID}" \
4 -d "client_secret=${APP_SECRET}" \
5 -d "fb_exchange_token=${SHORT_LIVED_USER_TOKEN}"1{
2 "access_token": "EAAGm0PX4ZCpsBA...",
3 "token_type": "bearer",
4 "expires_in": 5184000
5}5184000 seconds is 60 days. Meta's own hedge is worth repeating: “A long-lived token generally lasts about 60 days.” Treat it as approximate and read the real value rather than hardcoding the number.
This call takes your app secret, so it happens on your server. Meta says it plainly: “Make this call from your server, not a client.” An exchange in browser JavaScript ships your app secret to everyone who opens devtools.
There is no refresh endpoint for Facebook user tokens. Instagram has one. Facebook does not. When a long-lived user token reaches the end of its 60 days, the supported path is another login. This is a good reason to get to step 3 promptly and depend on the Page token instead, which is the one that does not expire.
Step 3: get the Page access token (the step people miss)
1curl -G "https://graph.facebook.com/v24.0/me/accounts" \
2 -d "fields=id,name,access_token,category,tasks" \
3 -d "access_token=${LONG_LIVED_USER_TOKEN}"1{
2 "data": [
3 {
4 "id": "102938475610293",
5 "name": "Your Page",
6 "access_token": "EAAGm0PX4ZCpsBA...",
7 "category": "Software Company",
8 "tasks": ["ADVERTISE", "ANALYZE", "CREATE_CONTENT", "MESSAGING", "MODERATE"]
9 }
10 ],
11 "paging": { "cursors": { "before": "MTAyOTM4", "after": "NDc1NjEw" } }
12}The lifetime is inherited, and nothing in the response tells you what you got.
Token you pass in | Page token you get back |
|---|---|
Long-lived user token (60 days) | Does not expire - subject to the invalidation table below |
Short-lived user token (1-2 hours) | Dies with it, in about an hour |
Same call, same fields, same shape. The only way to tell them apart is to inspect the token, which is the next section but one. If you are debugging an integration that “works for an hour”, this is your bug: someone called /me/accounts before doing the exchange in step 2.
Two more things this endpoint will do to you:
- It is paginated, and the default page size is 25. An agency user with 60 Pages will silently see 25 of them and assume the other 35 do not exist. Follow
paging.cursors.afteruntil it stops coming back. In production we request 100 per page and loop with a hard iteration cap, which is the shape you want - never a single unpaged call, never an unboundedwhile. - Check that
taskscontainsCREATE_CONTENTbefore you tell the user their Page is connected. A user can hold a Page role that lists the Page but cannot post to it, and finding that out at publish time is a bad user experience.
Instagram: two paths, and they are not interchangeable
Since Meta split the Instagram Platform APIs there are two supported ways to publish to an Instagram professional account. They use different hosts, different permissions and different token mechanics. Pick one deliberately - most of the confusion around Instagram tokens is people mixing documentation from both.
Path A: Instagram API with Instagram Login
Host is graph.instagram.com. The user logs in with Instagram; no Facebook Page is involved. Permissions are instagram_business_basic and instagram_business_content_publish, plus instagram_business_manage_comments and instagram_business_manage_insights if you need replies or metrics.
Exchange the short-lived token for a 60-day one:
1curl -G "https://graph.instagram.com/access_token" \
2 -d "grant_type=ig_exchange_token" \
3 -d "client_secret=${APP_SECRET}" \
4 -d "access_token=${SHORT_LIVED_IG_TOKEN}"And unlike Facebook, this path has a real refresh:
1curl -G "https://graph.instagram.com/refresh_access_token" \
2 -d "grant_type=ig_refresh_token" \
3 -d "access_token=${LONG_LIVED_IG_TOKEN}"Meta's constraint, exactly: “Refresh a long-lived access token that is at least 24 hours old but has not expired.” Both halves bite. Refresh at twelve hours and the call fails. Refresh on day 61 and there is nothing left to refresh - the user has to authorise again. Refresh on a schedule comfortably inside the window, not at day 59, and the account never has to think about it.
Path B: Instagram API with Facebook Login
Host is graph.facebook.com. The Instagram professional account is linked to a Facebook Page and you reach it through that Page:
1curl -G "https://graph.facebook.com/v24.0/${PAGE_ID}" \
2 -d "fields=instagram_business_account" \
3 -d "access_token=${PAGE_ACCESS_TOKEN}"Permissions are instagram_basic, instagram_content_publish and pages_read_engagement. The token mechanics are the Page token mechanics from step 3 - so the non-expiring Page token covers Instagram too, and there is no separate refresh cycle to run. Resumable video upload is only available on this path.
If this is you | Take |
|---|---|
Instagram only, no Facebook Pages in your product | Path A. Less setup, and a real refresh endpoint. |
You already handle Facebook Pages | Path B. Reuses the token you already hold, and adds resumable video upload. |
Large video uploads matter | Path B, or you will be fighting timeouts. |
For the record: Outstand runs Path A for Instagram and the Page-token flow for Facebook.
What actually invalidates a token
“Does not expire” is not the same as “never stops working”. Here is the full list of what kills a live token, what it takes down with it, and what you will see in your logs.
Trigger | What dies | What you see |
|---|---|---|
User changes their Facebook password | The user token, and every Page token derived from it |
|
User removes your app in their Facebook settings | Everything for that user |
|
You rotate the app secret | Every token your app ever issued, immediately |
|
A single permission is revoked | Nothing - the token still validates, the call fails |
|
The user loses their admin role on the Page | The Page token |
|
Meta security-flags the account | The user token |
|
The app is still in development mode | Nothing technically - it just never works for anyone outside your app roles | Login failure, or an empty |
Long-lived user token reaches ~60 days | The user token, and any short-lived Page token built from it |
|
90 days pass with no login from the user | Nothing - the token stays valid and data access lapses | Empty responses and no error at all |
The last two rows are the ones that cost people a weekend, because neither announces itself. The 90-day one is worse: the token still passes validation, so every check you wrote says the integration is healthy while it quietly returns nothing.
Verifying a token before you trust it
Never assume you got the token you meant to get. Ask:
1curl -G "https://graph.facebook.com/v24.0/debug_token" \
2 -d "input_token=${TOKEN_TO_INSPECT}" \
3 -d "access_token=${APP_ID}|${APP_SECRET}"1{
2 "data": {
3 "app_id": "1234567890",
4 "type": "PAGE",
5 "application": "Your App",
6 "data_access_expires_at": 1789200000,
7 "expires_at": 0,
8 "is_valid": true,
9 "scopes": ["pages_show_list", "pages_read_engagement", "pages_manage_posts"],
10 "profile_id": "102938475610293",
11 "user_id": "7654321098"
12 }
13}Three fields decide whether you are actually finished:
expires_at: 0is what a non-expiring Page token looks like. Any other value means you are on the short-lived path - go back and do the step 2 exchange first. This single check, run once at connection time, prevents the most common failure in this whole article.typeshould readPAGE, notUSER. If it saysUSERyou never called/me/accounts.scopesshould contain the permission you are about to rely on. Users can deselect individual permissions in the login dialog, and you find out here rather than at publish time.
And then the field almost everyone conflates with expiry.
expires_at is when the credential stops working. data_access_expires_at is when your app's permission to read that user's data lapses - roughly 90 days after they last used your app. These are different clocks and they run out at different times. A token can be entirely valid, return is_valid: true, and hand you nothing, because the second clock ran out. Only a fresh login resets it.
If an integration goes quiet after about three months with no error in the logs, this is almost always the reason.
Meta's browser-based Access Token Debugger shows the same data with a UI, which is fine for a one-off. debug_token is what belongs in your automated health check.
The production checklist
- Do the step 2 exchange server-side. It takes your app secret.
- Encrypt tokens at rest, and never send a Page token to the browser. A Page token that does not expire is a permanent credential to publish as that Page.
- Store
expires_atanddata_access_expires_atalongside the token, and alert on both. Monitoring only the first one is how the 90-day failure gets missed. - Branch on
codeanderror_subcode, never on HTTP status. 400 is not a synonym for transient here. - Handle
/me/accountspagination if any of your users manage more than 25 Pages. Some of them do. - Build the re-auth path on day one. Every token above dies eventually. The difference between a good integration and a bad one is whether the user gets a “reconnect” button or a silent failure.
- Do not hardcode a Page token you generated by hand. It is a credential with an owner and a revocation path, not a config value - and the person who generated it will eventually change their password.
- Re-read this when Meta ships a new Graph API version. The flow above is v24.0, verified 12 September 2026.
If you are running this on Outstand rather than building it yourself, the equivalent operational surface is documented under authentication, connect a new social network, finalize pending connection and check account health.
If you would rather not own this
This is the flow for one Meta app. Outstand runs this ladder for you on Facebook and Instagram, and the managed-OAuth equivalent on five more networks - LinkedIn, Threads, YouTube, Pinterest and TikTok - including re-auth when a token dies.
If you are building it yourself instead, bookmark the primary sources rather than a blog post - including this one. Meta's own pages on long-lived access tokens, Page access tokens, debug_token, Graph API error handling and Instagram content publishing are where all of the above came from.
Last verified: 12 September 2026 - Next review: 12 December 2026, against Graph API v24.0. Meta changes this flow, and a stale token guide is worse than no token guide, so this page is on a quarterly re-verification schedule.