You got a token from Facebook Login, everything works, and then exactly 1 hour later, your app goes dark. Sound familiar?
The Problem
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.
Prerequisites
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
Step-by-Step
Step 1 — Get an Authorization Code
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.
Step 2 — Exchange the Code for a Short-Lived Token
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.
Step 3 — Exchange Short-Lived Token for Long-Lived Token
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.
Step 4 — Refresh the Long-Lived Token (Before It Expires)
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.
Common Pitfalls
- 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 usesig_exchange_tokenfor the initial exchange andig_refresh_tokenfor 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.
TL;DR — The Full Flow
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.
Quick Reference Table
| Feature | THREADS | ||
|---|---|---|---|
| Authorize URL | www.facebook.com/v23.0/dialog/oauth |
api.instagram.com/oauth/authorize |
www.threads.com/oauth/authorize |
| Auth code exchange | POST graph.facebook.com/v23.0/oauth/access_token |
POST api.instagram.com/oauth/access_token |
POST graph.threads.net/oauth/access_token |
| Short → Long exchange URL | POST graph.facebook.com/v23.0/oauth/access_token |
GET graph.instagram.com/access_token |
GET graph.threads.net/access_token |
| Short → Long exchange grant type | fb_exchange_token |
ig_exchange_token |
th_exchange_token |
| Refresh URL | GET graph.facebook.com/v23.0/oauth/access_token |
GET graph.instagram.com/refresh_access_token |
GET graph.threads.net/refresh_access_token |
| Refresh grant type | fb_exchange_token |
ig_refresh_token |
th_refresh_token |
| 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 |
Dealing with the Meta API Headache?
If you've read this far, you know that Meta’s documentation is a moving target. Between versioned Graph API changes (like the jump to v23.0), inconsistent base URLs for Instagram, and the rigid 24-hour-to-60-day refresh window, token management becomes a high-maintenance sub-system in your codebase.
Beyond the tokens, getting your app through Meta's App Review and Business Verification is a multi-week process that requires screencasts, privacy policies, and strict data handling audits.
Consider a Unified API
If your goal is to build features rather than manage OAuth lifecycles and audit compliance, you might want to look at PostPulse for Developers.
We’ve built the PostPulse Social Media API specifically to solve these "last mile" integration problems. Instead of managing three different refresh flows for Facebook, Instagram, and Threads, you get:
- Managed Token Refresh: We handle the 24h/60-day logic for you.
- Unified Endpoints: One standard syntax for scheduling posts across 9+ platforms.
- Bypass App Review: Use our pre-approved Meta, LinkedIn, and TikTok integrations to go live in hours, not weeks.
Save your engineering hours for your core product. Try the PostPulse API for free →
Top comments (0)