Skip to main content
Socialift
Guide

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

Instagram token types and lifetimes — Capability matrix by platform
TokenSetupLifetimeWhere it comes from
Authorization codeInstagram Login1 hour, single useRedirect from instagram.com/oauth/authorize
Short-lived Instagram User tokenInstagram Login1 hourPOST api.instagram.com/oauth/access_token
Long-lived Instagram User tokenInstagram Login60 days, refreshableGET graph.instagram.com/access_token (ig_exchange_token)
Short-lived Facebook User tokenFacebook LoginA few hoursgraph.facebook.com/{version}/oauth/access_token with the code
Long-lived Facebook User tokenFacebook LoginAbout 60 daysSame endpoint, grant_type=fb_exchange_token
Page access tokenFacebook LoginNo expiration date when fetched with a long-lived user tokenGET /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

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

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

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

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

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

instagram_callback.py
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

refresh_tokens.py
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_at

AuthError 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

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

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

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

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

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

  6. Call Instagram endpoints

    Send requests to graph.facebook.com/{version}/<IG_ID>/... with the Page token.

The Facebook Login flow with the SDK

facebook_callback.py
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

Error 190 and other token errors — Capability matrix by platform
CodeMeaningWhat to do
190Access token is invalid or has expiredDon't retry; mark the account disconnected and ask the user to log in again
190 / 458The user has not logged into the appRe-authenticate
190 / 459The user is checkpointedThe user must log in at facebook.com first
190 / 460Password changedAsk the user to log in again
190 / 463Token expiredRe-authenticate; refresh earlier next time
190 / 464Unconfirmed userThe user must confirm the account on facebook.com
190 / 467Invalid access tokenGet a new token through login
10, 200–299Permission missing or not approvedFix 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 AuthError as a state change (reconnect required), not as a transient error to retry.
  • When a user disconnects, revoke permissions on Meta's side too (fb.revoke for Facebook Login) instead of only deleting the row.

← Back to blog

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.

Sources

  1. Meta: Business Login for Instagram
  2. Meta: Get long-lived access tokens
  3. Meta: Instagram API with Facebook Login — get started
  4. Meta: Graph API error handling
  5. Meta: debug_token reference
  6. Meta: Overview of the Instagram API

Related pages