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

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

Published on March 10, 2026

Tags:

Meta API
Auth & Security

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

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 (same!)

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

About PostPulse

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 →

About the Author

Oleksandr Pohorelov
Oleksandr Pohorelov

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.