
Published on March 10, 2026
Tags:
You got a token from Facebook Login, everything works, and then exactly 1 hour later, your app goes dark. Sound familiar?
Here's what trips up almost every developer integrating with Facebook, Instagram, or Threads for the first time: you complete the OAuth flow, get a shiny access token, make a few API calls, and everything is fine — until it isn't. An hour later, your calls start returning OAuthException errors, and you're staring at your code, wondering what went wrong.
The answer? Meta issues a short-lived token by default. It's valid for about 1–2 hours, and most tutorials end there. What they don't tell you is that there's a second exchange step to get a long-lived token (valid for 60 days), and a third step to refresh that long-lived token before it expires.
Let's walk through the full lifecycle across Facebook, Instagram, and Threads.
Before you start, make sure you have:
A Facebook App created at Meta for Developers
App ID (client_id) and App Secret (client_secret)
For Instagram/Threads: the appropriate product has been added to your Meta App.
OAuth redirect URI configured in your app settings.
Required permissions/scopes:
Facebook: pages_manage_posts, pages_read_engagement, public_profile
Instagram: instagram_basic, instagram_content_publish
Threads: threads_basic, threads_content_publish
Direct the user to the platform-specific authorization URL:
Facebook:
https://www.facebook.com/v23.0/dialog/oauth
?client_id={app_id}
&redirect_uri={your_redirect_uri}
&scope=pages_manage_posts,pages_read_engagement,public_profile
&response_type=code
&state={csrf_token}Instagram:
https://api.instagram.com/oauth/authorize
?client_id={app_id}
&redirect_uri={your_redirect_uri}
&scope=instagram_basic,instagram_content_publish
&response_type=code &state={csrf_token}Threads:
https://www.threads.com/oauth/authorize
?client_id={app_id}
&redirect_uri={your_redirect_uri}
&scope=threads_basic,threads_content_publish
&response_type=code
&state={csrf_token}After the user grants permissions, Meta redirects back with a `code` parameter. This code is single-use and expires in about 10 minutes.
Facebook:
curl -X POST "https://graph.facebook.com/v23.0/oauth/access_token" \
-d "code={authorization_code}" \
-d "grant_type=authorization_code" \
-d "client_id={app_id}" \
-d "redirect_uri={your_redirect_uri}" \
-d "client_secret={app_secret}"Instagram:
curl -X POST "https://api.instagram.com/oauth/access_token" \
-d "code={authorization_code}" \
-d "grant_type=authorization_code" \
-d "client_id={app_id}" \
-d "redirect_uri={your_redirect_uri}" \
-d "client_secret={app_secret}"Threads:
curl -X POST "https://graph.threads.net/oauth/access_token" \
-d "code={authorization_code}" \
-d "grant_type=authorization_code" \
-d "client_id={app_id}" \
-d "redirect_uri={your_redirect_uri}" \
-d "client_secret={app_secret}"Response:
{
"access_token": "EAAG...short-lived-token...",
"token_type": "bearer",
"expires_in": 3600
}⚠️ This is the short-lived token. It expires in ~1 hour. If you stop here, your integration will break every 60 minutes. Don’t stop here.
This is the step most tutorials skip. You take the short-lived token from Step 2 and exchange it for a long-lived one:
Facebook:
curl -X POST "https://graph.facebook.com/v23.0/oauth/access_token" \
-d "grant_type=fb_exchange_token" \
-d "client_id={app_id}" \
-d "client_secret={app_secret}" \
-d "fb_exchange_token={short_lived_token}"Instagram:
curl -G "https://graph.instagram.com/access_token" \
-d "grant_type=ig_exchange_token" \
-d "client_secret={app_secret}" \
-d "access_token={short_lived_token}"Threads:
curl -G "https://graph.threads.net/access_token" \
-d "grant_type=th_exchange_token" \
-d "client_secret={app_secret}" \
-d "access_token={short_lived_token}"Response:
{
"access_token": "EAAG...long-lived-token...",
"token_type": "bearer",
"expires_in": 5184000
}That 5184000 is 60 days in seconds. You just went from 1 hour to 60 days.
⚠️ Notice the different grant types: Facebook uses fb_exchange_token, while Instagram uses ig_exchange_token, and Threads uses th_exchange_token. They’re the same concept but different parameter values. Mix them up, and you’ll get an unhelpful error.
Long-lived tokens can be refreshed for another 60 days. But there are conditions:
The token must be at least 24 hours old
The token must not have expired yet
Facebook:
curl -G "https://graph.facebook.com/v23.0/oauth/access_token" \
-d "grant_type=fb_exchange_token" \
-d "client_id={app_id}" \
-d "client_secret={app_secret}" \
-d "fb_exchange_token={long_lived_token}"Instagram:
curl -G "https://graph.instagram.com/refresh_access_token" \
-d "grant_type=ig_refresh_token" \
-d "access_token={long_lived_token}"`Threads:
curl -G "https://graph.threads.net/refresh_access_token" \
-d "grant_type=th_refresh_token" \
-d "access_token={long_lived_token}"💡 Pro tip: Set up a scheduled job to refresh tokens that are between 24 hours and 59 days old. If you miss the window, the user will need to re-authenticate from scratch.
Stopping at the short-lived token — The #1 mistake. Your app will work for an hour in development and then break in production. Always exchange for long-lived.
Mixing up grant types — Facebook uses fb_exchange_token, Instagram uses ig_exchange_token for the initial exchange and ig_refresh_token for refresh. They look similar but they’re not interchangeable.
Trying to refresh too early — If the long-lived token is less than 24 hours old, the refresh call will silently return the same token with the same expiry. It won’t error — it just won’t do anything.
Trying to refresh an expired token — Once expired, you can’t refresh it. The user must go through the full OAuth flow again.
Not storing the refresh token separately — For Meta, the long-lived token IS the refresh token. You exchange the existing long-lived token for a new long-lived token. This is different from platforms like Twitter or TikTok which give you separate access_token and refresh_token values.
Forgetting about Page Access Tokens — If you’re posting to Facebook Pages, you also need a Page Access Token (covered in the next article). A user token alone won’t let you post to a page.
Not handling Instagram’s different base URLs — The initial token exchange goes to api.instagram.com, but the long-lived exchange and refresh go to graph.instagram.com. Different hosts for different operations.
The complete Meta OAuth token lifecycle:
User clicks “Login” → Redirect to platform's authorization URL.
User grants permissions → Redirect back with ?code=... (expires in ~10 min)
POST /oauth/access_token → Short-lived token (~1 hour)
⭐ THE STEP EVERYONE MISSES: Exchange with fb_exchange_token (FB), ig_exchange_token (IG), or th_exchange_token (Threads) → Long-lived token (60 days)
Store token. Set up a refresh job.
Refresh every ~30 days (must be ≥24h old, must not be expired). FB: fb_exchange_token, IG: ig_refresh_token, Threads: th_refresh_token.
Threads | |||
|---|---|---|---|
Authorize URL |
|
|
|
Auth code exchange |
|
|
|
Short → Long exchange URL |
|
|
|
Short → Long exchange grant type |
|
|
|
Refresh URL |
|
|
|
Refresh grant type |
|
|
|
Token lifetime | 60 days | 60 days | 60 days |
Refresh window | 24h after creation → before expiry | 24h after creation → before expiry | 24h after creation → before expiry |
PostPulse is a social media scheduling platform that handles all of this automatically — token lifecycle management across 9+ platforms, so you never have to worry about expired tokens breaking your scheduled posts. Try PostPulse →
Founder of PostPulse — a social media scheduling platform for creators and teams. Software engineer with a passion for building developer tools and simplifying complex API integrations across social media platforms.