Developer guide ·

Threads API Access Tokens Explained

Every Threads API call needs an access token. This guide explains the token types and how long they last, how to get one through OAuth, how to turn it into a 60-day token and keep refreshing it, how to store it safely, and how to fix token errors.

SHORT-LIVED TOKEN 1 hour th_exchange_token LONG-LIVED TOKEN 60 days valid · refreshable 🔄 refreshed · +60 days 🔒 server-side only
1 hourshort-lived token from the OAuth code
60 dayslong-lived token after exchange
24 hoursminimum token age before you can refresh
0ways to revive an expired token - refresh in time
Token types

What is a Threads API access token?

An access token is a string your app sends with every request to prove a specific Threads user has granted it permission. It's tied to one user, one app and the permissions that user approved. There are two kinds you'll work with.

Short-lived

Short-lived user token

Lifetime
1 hour
Comes from
Exchanging the OAuth authorization code
Use it for
Immediately exchanging for a long-lived token
Can refresh?
No
Long-lived

Long-lived user token

Lifetime
60 days
Comes from
GET /access_token with grant_type=th_exchange_token
Use it for
All your app's API calls on the user's behalf
Can refresh?
Yes - after 24 hours, before expiry, for another 60 days

A token carries

  • The Threads user ID it acts for
  • The permissions (scopes) that user granted
  • Your app's identity
  • An expiry time

A token is not

  • Your app ID or app secret
  • The user's password
  • Shareable between users or apps
  • Safe to put in browser code or a public repo
Getting a token

How to get a Threads API access token

There are two routes: a quick token for testing on your own account, and the full OAuth flow for real users.

Route A · Testing

Generate a token for a tester

  1. In your Meta app, open the Threads use case settings.
  2. Add your Threads account under Roles → Threads testers and accept the invite in the Threads app (Settings → Account → Website permissions).
  3. Use the dashboard's user token generator, or the Graph API Explorer, to create a token for that tester.

💡 Ideal for prototypes and scripts that only post to your own account. Exchange it for a long-lived token the same way as below.

Route B · Production

Use the OAuth 2.0 flow

  1. Send the user to the authorization window with your app ID, redirect URI, scopes and a random state.
  2. They approve; Threads redirects back with a code.
  3. Your server checks state, then exchanges the code for a short-lived token and user ID.
  4. Immediately exchange it for a long-lived token and store it.

💡 Authorization codes are single-use and expire quickly - exchange them right away.

# pip install flask requests
import os, secrets, time, requests
from urllib.parse import urlencode
from flask import Flask, redirect, request, session, abort

app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET"]

APP_ID = os.environ["THREADS_APP_ID"]
APP_SECRET = os.environ["THREADS_APP_SECRET"]
REDIRECT_URI = "https://yourapp.com/callback"   # must match app settings exactly
SCOPES = "threads_basic,threads_content_publish"

@app.route("/login")
def login():
    session["state"] = secrets.token_urlsafe(24)
    params = {"client_id": APP_ID, "redirect_uri": REDIRECT_URI,
              "scope": SCOPES, "response_type": "code",
              "state": session["state"]}
    return redirect("https://threads.net/oauth/authorize?" + urlencode(params))

@app.route("/callback")
def callback():
    if request.args.get("state") != session.pop("state", None):
        abort(400, "State mismatch")
    code = request.args["code"].removesuffix("#_")

    # 1) code -> short-lived token (1 hour)
    short = requests.post("https://graph.threads.net/oauth/access_token", data={
        "client_id": APP_ID, "client_secret": APP_SECRET,
        "grant_type": "authorization_code",
        "redirect_uri": REDIRECT_URI, "code": code}).json()

    # 2) short-lived -> long-lived token (60 days)
    long = requests.get("https://graph.threads.net/access_token", params={
        "grant_type": "th_exchange_token", "client_secret": APP_SECRET,
        "access_token": short["access_token"]}).json()

    save_token(user_id=short["user_id"], token=long["access_token"],
               expires_at=time.time() + long["expires_in"])
    return "Connected to Threads ✓"

def save_token(user_id, token, expires_at):
    ...  # store encrypted in your database
// npm install express express-session
import express from "express";
import session from "express-session";
import crypto from "node:crypto";

const { THREADS_APP_ID: APP_ID, THREADS_APP_SECRET: APP_SECRET } = process.env;
const REDIRECT_URI = "https://yourapp.com/callback"; // must match app settings
const SCOPES = "threads_basic,threads_content_publish";

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

app.get("/login", (req, res) => {
  req.session.state = crypto.randomBytes(24).toString("base64url");
  const params = new URLSearchParams({
    client_id: APP_ID, redirect_uri: REDIRECT_URI, scope: SCOPES,
    response_type: "code", state: req.session.state,
  });
  res.redirect(`https://threads.net/oauth/authorize?${params}`);
});

app.get("/callback", async (req, res) => {
  if (!req.query.state || req.query.state !== req.session.state) {
    return res.status(400).send("State mismatch");
  }
  delete req.session.state;
  const code = String(req.query.code).replace(/#_$/, "");

  // 1) code -> short-lived token (1 hour)
  const short = await fetch("https://graph.threads.net/oauth/access_token", {
    method: "POST",
    body: new URLSearchParams({
      client_id: APP_ID, client_secret: APP_SECRET,
      grant_type: "authorization_code", redirect_uri: REDIRECT_URI, code,
    }),
  }).then((r) => r.json());

  // 2) short-lived -> long-lived token (60 days)
  const qs = new URLSearchParams({
    grant_type: "th_exchange_token", client_secret: APP_SECRET,
    access_token: short.access_token,
  });
  const long = await fetch(`https://graph.threads.net/access_token?${qs}`).then((r) => r.json());

  await saveToken(short.user_id, long.access_token, Date.now() + long.expires_in * 1000);
  res.send("Connected to Threads ✓");
});

async function saveToken(userId, token, expiresAt) {
  /* store encrypted in your database */
}

app.listen(3000);
# 1) Open in a browser (user approves, you get ?code=... on your redirect URI)
https://threads.net/oauth/authorize?client_id=YOUR_APP_ID&redirect_uri=https://yourapp.com/callback&scope=threads_basic,threads_content_publish&response_type=code&state=RANDOM_STRING

# 2) Exchange the code for a short-lived token (1 hour)
curl -X POST https://graph.threads.net/oauth/access_token \
  -d client_id=YOUR_APP_ID \
  -d client_secret=YOUR_APP_SECRET \
  -d grant_type=authorization_code \
  -d redirect_uri=https://yourapp.com/callback \
  -d code=CODE_FROM_REDIRECT
# → { "access_token": "SHORT_LIVED", "user_id": 1234567890 }

If the code in your redirect URL ends with #_, strip those characters before exchanging it. Use the exact same redirect_uri in the authorize URL, the exchange call and your app settings - a mismatch is the most common setup error.

Long-lived tokens

Exchange for a long-lived Threads token

A short-lived token expires in an hour, so swap it for a 60-day token straight away. This call must run on your server because it uses your app secret - and an expired short-lived token can't be exchanged, so do it before the hour is up.

Exchange: short → long-lived
curl -G https://graph.threads.net/access_token \
  -d grant_type=th_exchange_token \
  -d client_secret=YOUR_APP_SECRET \
  -d access_token=SHORT_LIVED_TOKEN

# → {
#     "access_token": "LONG_LIVED_TOKEN",
#     "token_type": "bearer",
#     "expires_in": 5183944     // seconds ≈ 60 days
#   }
Verify a token works
curl -G https://graph.threads.net/v1.0/me \
  -d fields=id,username \
  -d access_token=LONG_LIVED_TOKEN

# Valid token  → { "id": "1234567890", "username": "yourbrand" }
# Bad/expired  → { "error": { "code": 190, "message": "...",
#                             "fbtrace_id": "..." } }
Refreshing

How to refresh a Threads access token

A long-lived token can be refreshed once it's at least 24 hours old and not yet expired; the user must still have granted threads_basic. Each refresh returns a token valid for another 60 days. Miss the deadline and the user has to log in again.

Day 01020304050Day 60 🔒 24h lock ✓ Refresh allowed ⏰ Refresh by here ✕ Expires 🔑 Refreshed on day 50 → new 60-day token, timeline resets
Refresh a long-lived token
curl -G https://graph.threads.net/refresh_access_token \
  -d grant_type=th_refresh_token \
  -d access_token=LONG_LIVED_TOKEN

# → {
#     "access_token": "NEW_LONG_LIVED_TOKEN",
#     "token_type": "bearer",
#     "expires_in": 5183944
#   }
# Requirements: token is 24h+ old, not expired,
# and the user still grants threads_basic.
Daily refresh job (Python)
# Run once a day (cron, scheduler, etc.)
import time, requests

DAY = 86400
REFRESH_AFTER = 50 * DAY      # refresh tokens older than ~50 days

def refresh_due_tokens(db):
    now = time.time()
    for row in db.tokens_issued_before(now - REFRESH_AFTER):
        r = requests.get("https://graph.threads.net/refresh_access_token",
                         params={"grant_type": "th_refresh_token",
                                 "access_token": row.token})
        data = r.json()
        if r.ok and "access_token" in data:
            db.update(row.user_id,
                      token=data["access_token"],
                      issued_at=now,
                      expires_at=now + data["expires_in"])
        else:
            db.flag_reauth(row.user_id, data.get("error"))

Run the job daily and refresh anything older than ~50 days, not only tokens about to expire. That leaves a margin for outages and retries. Save the new token and its new expiry each time.

Calculator

Threads token expiry calculator

Enter when a long-lived token was issued or last refreshed to see when you can refresh it, when you should, and when it expires. Times are shown in your local time zone.

Can refresh from-
Refresh by-
Expires-
Status-

Security

How to store Threads access tokens safely

A token is a key to someone's account. Treat it like a password.

THQVJ… 🔒 encrypted at rest
🗄️

Keep tokens on the server

Store them in your database, encrypted at rest, or in a secrets manager. Never put them in browser JavaScript, mobile app bundles, URLs or logs.

🔐

Protect the app secret

The app secret is needed for code and token exchanges. Keep it in environment variables, rotate it if it ever leaks, and never commit it to Git.

🧾

Save the metadata

Store the Threads user ID, granted scopes, issue time and expiry with each token so your refresh job and permission checks have what they need.

🎯

Ask for the fewest scopes

Only request permissions you use. Fewer scopes mean less damage if a token leaks - and an easier App Review.

🧹

Handle uninstalls and deletion

Users can remove your app from their Threads settings. Set the uninstall and data-deletion callback URLs in your app settings, and delete stored tokens when you're notified.

Errors

Common Threads token errors and fixes

What you seeLikely causeFix
Error code 190 · invalid or expired tokenToken expired, was revoked, or the user changed their password or removed your appSend the user through OAuth again; refresh tokens earlier in future
"Session has expired"A short-lived token older than 1 hour, or a long-lived token past 60 daysExchange short-lived tokens immediately; schedule refreshes
Refresh call rejectedToken is less than 24 hours old, already expired, or threads_basic is missingWait 24 hours after issue; if expired, re-authorize
"Invalid redirect_uri"The URI differs between the authorize URL, exchange call and app settingsUse one identical URI everywhere, including the trailing slash
Authorization code rejectedCode already used, expired, or has a trailing #_Exchange the code once, right away, after stripping #_
Permission error on a callThe user didn't grant that scope, or your app lacks advanced accessRequest the scope in OAuth; get advanced access through App Review

Always log the error code, error_subcode and fbtrace_id from failed calls - they make support requests and debugging much faster. More in our errors & rate limits guide.

FAQ

Threads access token FAQ

How long does a Threads access token last?
A short-lived token from the OAuth flow lasts 1 hour. A long-lived token lasts 60 days and can be refreshed for another 60 days once it's at least 24 hours old, as long as it hasn't expired.
How do I get a long-lived Threads token?
Call GET https://graph.threads.net/access_token with grant_type=th_exchange_token, your app's client_secret, and the unexpired short-lived token as access_token. Do this server-side.
How do I refresh a Threads access token?
Call GET https://graph.threads.net/refresh_access_token with grant_type=th_refresh_token and the current long-lived token. It must be at least 24 hours old and not expired. You'll get a new token valid for 60 days.
What happens when a Threads token expires?
API calls fail with an invalid-token error (code 190) and the token can't be refreshed. The user has to go through the OAuth authorization flow again to get a new token.
Can I use one token for multiple Threads accounts?
No. Each token belongs to one Threads user and one app. To manage several accounts, store a separate token for each user who authorizes your app.
Where should I store Threads access tokens?
On your server only - encrypted in a database or in a secrets manager - along with the user ID, scopes and expiry. Never expose tokens or your app secret in browser code, mobile bundles, URLs or logs.