How to get and refresh an Instagram access token
An Instagram access token starts as an OAuth code: you exchange it for a short-lived token (1 hour) and then for a long-lived one (60 days). With Instagram Login you refresh that token at graph.instagram.com once it is at least 24 hours old. With Facebook Login you get a long-lived user token, then read the Page token and Instagram account ID from the Page.
Updated:
Which Instagram access token do you need?
It depends on the setup. Instagram API with Instagram Login uses an Instagram User access token issued by instagram.com and called against graph.instagram.com. Instagram API with Facebook Login uses Facebook tokens on graph.facebook.com: a user token to list Pages and a Page access token to call Instagram endpoints. Each token family works with its own host.
Every token request that includes your app secret must run on your server. Meta states this explicitly for both the Instagram long-lived exchange and Facebook's fb_exchange_token call.
The table below puts every token type in one place — most integration bugs come from mixing up the token type or the host.
Instagram token types and lifetimes
| Token | Setup | Lifetime | Where it comes from |
|---|---|---|---|
| Authorization code | Instagram Login | 1 hour, single use | Redirect from instagram.com/oauth/authorize |
| Short-lived Instagram User token | Instagram Login | 1 hour | POST api.instagram.com/oauth/access_token |
| Long-lived Instagram User token | Instagram Login | 60 days, refreshable | GET graph.instagram.com/access_token (ig_exchange_token) |
| Short-lived Facebook User token | Facebook Login | A few hours | graph.facebook.com/{version}/oauth/access_token with the code |
| Long-lived Facebook User token | Facebook Login | About 60 days | Same endpoint, grant_type=fb_exchange_token |
| Page access token | Facebook Login | No expiration date when fetched with a long-lived user token | GET /me/accounts |
Even a non-expiring Page token can be invalidated under certain conditions — the error 190 subcodes below show the typical reasons.
Instagram Login: from code to long-lived token
Send the user to the login dialog
Redirect to https://www.instagram.com/oauth/authorize with client_id, redirect_uri, response_type=code, a comma-separated scope (e.g. instagram_business_basic,instagram_business_manage_messages) and a random state.
Receive the code
Instagram redirects back to redirect_uri with ?code=...; the trailing #_ is not part of the code, so strip it. The code is valid for 1 hour and can be used once.
Exchange it for a short-lived token
POST the form fields client_id, client_secret, grant_type=authorization_code, redirect_uri and code to https://api.instagram.com/oauth/access_token. The response holds the short-lived token, the user_id and the granted permissions.
Get a long-lived token
GET https://graph.instagram.com/access_token with grant_type=ig_exchange_token, client_secret and access_token. expires_in is roughly 5,184,000 seconds — 60 days.
Store it with its expiry
Save the token encrypted together with expires_at (now + expires_in). The refresh job will need that date.
The same flow with the Socialift SDK
import os
from socialift import Instagram
async def instagram_callback(code: str, state: str, expected_state: str) -> dict:
if state != expected_state:
raise ValueError("state mismatch") # CSRF protection
async with Instagram.from_env() as ig:
bundle = await ig.exchange_code(code, redirect_uri=os.environ["IG_REDIRECT_URI"])
profile = await ig.profile(bundle.access_token)
return {
"username": profile.username,
"token": bundle.access_token, # encrypt before saving
"expires_at": bundle.expires_at, # about 60 days from now
}exchange_code performs both steps: the form-encoded POST to api.instagram.com and the ig_exchange_token call. It returns a TokenBundle with access_token, expires_in and a computed expires_at.
ig.login_url(redirect_uri=..., state=...) builds the dialog URL with the SDK's default instagram_business_* scopes; pass scopes=[...] to narrow them. The SDK does not store state — keep it in the session.
How do you refresh an Instagram long-lived token?
Call GET https://graph.instagram.com/refresh_access_token with grant_type=ig_refresh_token and the current token. The response is a new long-lived token, again valid for about 60 days.
Three conditions apply: the token is at least 24 hours old, it has not expired, and the user granted instagram_business_basic. A token that is not refreshed within 60 days expires for good and cannot be refreshed — the user has to log in again.
In practice a daily or weekly cron job that refreshes tokens expiring within the next week or two is enough. Refreshing is cheap; losing an account because the cron job was down is not.
A token refresh job
from datetime import datetime, timedelta, timezone
from socialift import AuthError, Instagram
async def refresh_instagram_tokens(accounts) -> None:
soon = datetime.now(timezone.utc) + timedelta(days=10)
async with Instagram.from_env() as ig:
for acc in accounts: # rows from your own database
if acc.expires_at > soon:
continue
try:
bundle = await ig.refresh_token(acc.token)
except AuthError:
acc.status = "reconnect_required" # expired or revoked
continue
acc.token, acc.expires_at = bundle.access_token, bundle.expires_atAuthError is not fixed by retrying: move the account to a "reconnect required" state and notify the user.
Facebook Login: user token → Page token → Instagram account ID
Log in with Facebook
Send the user through Facebook Login (for Business apps, with a configuration ID) and request instagram_basic, pages_show_list and the permissions your feature needs.
Exchange the code
GET /oauth/access_token on graph.facebook.com with client_id, client_secret, redirect_uri and code returns a short-lived user token that expires within hours.
Get a long-lived user token
Call the same endpoint with grant_type=fb_exchange_token, client_id, client_secret and fb_exchange_token. The result generally lasts about 60 days.
List Pages and their tokens
GET /me/accounts with the long-lived user token returns the Pages where the user can perform MANAGE, CREATE_CONTENT, MODERATE or ADVERTISE, each with an access_token. Page tokens obtained this way have no expiration date.
Find the Instagram account
GET /{page-id}?fields=instagram_business_account returns the ID of the linked Instagram professional account — the ID you use in every Instagram call.
Call Instagram endpoints
Send requests to graph.facebook.com/{version}/<IG_ID>/... with the Page token.
The Facebook Login flow with the SDK
import os
from socialift import Facebook, Instagram
async def facebook_callback(code: str) -> list[dict]:
async with Facebook.from_env() as fb:
user = await fb.exchange_code(code, redirect_uri=os.environ["FB_REDIRECT_URI"])
print("granted:", await fb.granted_scopes(user.access_token))
pages = await fb.pages(user.access_token)
accounts = [p for p in pages if p.instagram_id]
async with Instagram.via_facebook(app_secret=os.environ["FACEBOOK_APP_SECRET"]) as ig:
for page in accounts:
profile = await ig.profile(page.access_token, ig_user_id=page.instagram_id)
print(page.name, "->", profile.username)
return [{"page_id": p.id, "ig_user_id": p.instagram_id, "page_token": p.access_token} for p in accounts]fb.exchange_code runs both exchanges in one call and returns the long-lived user token; redirect_uri must match the login URL character for character.
fb.pages asks for instagram_business_account inside the same /me/accounts call, so there is no extra request per Page.
fb.granted_scopes reads debug_token: users can untick permissions in the dialog, so check what was actually granted.
How do you debug an Instagram access token?
For Facebook tokens call GET /debug_token?input_token=<token> with an app access token ({app-id}|{app-secret}) or an app developer's user token. The response includes is_valid, expires_at, data_access_expires_at, scopes, granular_scopes (with target objects such as Page IDs) and user_id. The Access Token Debugger in Meta's developer tools shows the same data.
For an Instagram User token the quickest health check is a cheap call such as GET graph.instagram.com/me?fields=user_id,username. A normal response means the token works; error 190 means the user has to reconnect.
Log the error's code, error_subcode and fbtrace_id — never the token itself. fbtrace_id is what Meta support asks for.
Error 190 and other token errors
| Code | Meaning | What to do |
|---|---|---|
| 190 | Access token is invalid or has expired | Don't retry; mark the account disconnected and ask the user to log in again |
| 190 / 458 | The user has not logged into the app | Re-authenticate |
| 190 / 459 | The user is checkpointed | The user must log in at facebook.com first |
| 190 / 460 | Password changed | Ask the user to log in again |
| 190 / 463 | Token expired | Re-authenticate; refresh earlier next time |
| 190 / 464 | Unconfirmed user | The user must confirm the account on facebook.com |
| 190 / 467 | Invalid access token | Get a new token through login |
| 10, 200–299 | Permission missing or not approved | Fix the requested scopes or App Review; retries won't help |
In the Socialift SDK codes 190 and 102 raise AuthError, codes 10 and 200–299 raise PermissionError; both carry code and subcode from Meta's response.
How to store Instagram tokens safely
In the Socialift dashboard, OAuth tokens of connected accounts are encrypted on the server and never sent to the browser. Apply the same rules in your own backend:
- Keep the app secret and every token exchange on the server; the secret never reaches a browser or mobile app.
- Store tokens encrypted at rest, each with its expires_at.
- Never write tokens to logs or error trackers — log the account ID and fbtrace_id instead.
- Generate a random state for every login and compare it in the callback to block CSRF.
- Treat
AuthErroras a state change (reconnect required), not as a transient error to retry. - When a user disconnects, revoke permissions on Meta's side too (
fb.revokefor Facebook Login) instead of only deleting the row.
Frequently asked questions
How long does an Instagram access token last?
With Instagram Login a short-lived token lasts 1 hour and a long-lived token 60 days; each refresh grants another 60 days. With Facebook Login a long-lived user token lasts about 60 days, and Page tokens obtained with it have no expiration date.
Can I refresh an expired Instagram token?
No. refresh_access_token only accepts a long-lived token that is at least 24 hours old and still valid. A token not refreshed within 60 days expires for good, and the user has to go through login again.
What does Instagram API error 190 mean?
Error 190 is an OAuthException: the access token is invalid, expired or revoked. error_subcode explains why (463 expired, 460 password changed, 458 not logged into the app). Retrying won't help — you need a new token.
Why doesn't my Instagram User token work on graph.facebook.com?
Instagram Login tokens are for graph.instagram.com, while Facebook Login uses Facebook User and Page tokens on graph.facebook.com. Use the host that matches the login flow that issued the token.
Do I need a Page token for Instagram?
Only with Facebook Login. There, Instagram endpoints are called with the token of the Page linked to the account, and the Instagram account ID comes from the Page's instagram_business_account field. Instagram Login needs no Page at all.