DEV Community

Cover image for Meta OAuth: Short-Lived vs Long-Lived Tokens (and Why Your Token Expires After 1 Hour)
Oleksandr Pohorelov
Oleksandr Pohorelov

Posted on

Meta OAuth: Short-Lived vs Long-Lived Tokens (and Why Your Token Expires After 1 Hour)

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:

  1. A Facebook App created at Meta for Developers
  2. App ID (client_id) and App Secret (client_secret)
  3. For Instagram/Threads: the appropriate product has been added to your Meta App.
  4. OAuth redirect URI configured in your app settings.
  5. Required permissions/scopes:
  6. Facebook: pages_manage_posts, pages_read_engagement, public_profile
  7. Instagram: instagram_basic, instagram_content_publish
  8. 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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

Response:

{
"access_token": "EAAG...short-lived-token...", 
"token_type": "bearer", 
"expires_in": 3600
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 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}"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

Response:

{
"access_token": "EAAG...long-lived-token...", 
"token_type": "bearer", 
"expires_in": 5184000
}
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

Instagram:

curl -G "https://graph.instagram.com/refresh_access_token" \  
-d "grant_type=ig_refresh_token" \  
-d "access_token={long_lived_token}"
Enter fullscreen mode Exit fullscreen mode

Threads:

curl -G "https://graph.threads.net/refresh_access_token" \  
-d "grant_type=th_refresh_token" \  
-d "access_token={long_lived_token}"
Enter fullscreen mode Exit fullscreen mode

💡 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 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.

TL;DR — The Full Flow

The complete Meta OAuth token lifecycle:

  1. User clicks “Login” → Redirect to platform's authorization URL.
  2. User grants permissions → Redirect back with ?code=... (expires in ~10 min)
  3. POST /oauth/access_token → Short-lived token (~1 hour)
  4. ⭐ THE STEP EVERYONE MISSES: Exchange with fb_exchange_token (FB), ig_exchange_token (IG), or th_exchange_token (Threads) → Long-lived token (60 days)
  5. Store token. Set up a refresh job.
  6. 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 FACEBOOK INSTAGRAM 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)