updated 3 November 2026

Facebook Page access tokens, explained

A Facebook Page access token lets your server publish and read as a Page through the Graph API. You can't create one directly: a person with a task on the Page logs in to your app, you exchange their user token for a long-lived one, then call /me/accounts. Page tokens made from a long-lived user token have no expiration date, though Meta can still invalidate them.

Which Facebook access token you need

Meta issues several kinds of access token, and Graph API calls fail in confusing ways when you send the wrong one (access tokens):

TokenWhat it is for
User access tokenActing for a person who logged in to your app and granted permissions. You need one to get a Page token.
Page access tokenReading and writing a Page's data: posts, comments, Insights. Unique to each Page, user, and app combination.
System user access tokenAutomated actions on Pages or ad objects owned by your own business portfolio, without a person logging in.
App access tokenApp settings and server calls such as debug_token. Made from your app ID and secret.
Client tokenIdentifying a native or desktop app. Not secret, and not usable on its own.

To post to a Page, use a Page access token. A user token, even one with pages_manage_posts, can't post as the Page.

How long each token lasts

Short-lived tokens typically last one to two hours and long-lived ones about 60 days. Meta warns that these lifetimes can change without notice and that tokens can expire early (access tokens). For Pages the rules chain together (long-lived tokens):

You start withYou get
Web loginA short-lived user token
A short-lived user tokenA short-lived Page token from /me/accounts
A short-lived user token plus your app secretA long-lived user token (about 60 days) from fb_exchange_token
A long-lived user tokenA Page token with no expiration date from /me/accounts

So the order matters: exchange the user token first, then list Pages. You can't exchange a token that has already expired; the person has to log in again. The exchanged token is a different string, so replace the old one wherever you store it.

Permissions and Page tasks

Two separate checks decide what a Page token can do: the permissions the person granted your app, and the tasks that person can perform on the Page.

PermissionWhat it unlocks
pages_show_listThe list of Pages a person manages, which is what /me/accounts returns
pages_manage_postsCreating, editing, and deleting Page posts, photos, and videos. Depends on pages_read_engagement and pages_show_list
pages_read_engagementReading the Page's content, followers, and metadata
read_insightsReading Page Insights data. Depends on pages_read_engagement and pages_show_list
pages_manage_engagementCreating, editing, and deleting the Page's own comments, such as replies, and likes on Page posts
pages_read_user_contentReading comments and posts that people and other Pages leave on the Page, and deleting user comments
publish_videoPublishing video to the Page

Those come from Meta's permissions reference and posts guide. Meta's getting started page also asks for pages_manage_metadata. To publish, the first three are the core set. Add the two comment permissions only if you read or answer comments, publish_video only for video, and read_insights only for Insights: App Review asks you to justify each one.

Tasks are per person and per Page. Publishing needs CREATE_CONTENT, replying to and deleting comments needs MODERATE, Insights needs ANALYZE, and a Page admin can do all of them (Pages API overview). Each Page that /me/accounts returns lists the person's tasks, so check for CREATE_CONTENT before you offer a Page as a posting target.

Every Page permission needs App Review before people without a role on your app can grant it once the app is live. In development mode, only people with a role on the app can. Business apps have no app modes: each permission needs Advanced access before someone with neither a role on the app nor a role in the business that claimed it can grant it (Pages API overview).

Get a long-lived Page access token, step by step

  1. Create a Meta app, add Facebook Login (or Facebook Login for Business, which Meta recommends for apps that manage business clients' assets), and request the permissions above.
  2. Send the person through the login dialog. Your callback gets a short-lived user token.
  3. On your server, exchange it for a long-lived user token. This call includes your app secret, so it never runs in a browser or mobile app (long-lived tokens).
    curl -G "https://graph.facebook.com/v26.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_USER_TOKEN>"
    # {"access_token": "<LONG_LIVED_USER_TOKEN>", "token_type": "bearer", "expires_in": 5183944}
  4. Call /me/accounts with the long-lived user token. Each entry has the Page ID, name, the person's tasks, and a Page access token. The list is paginated, and only shows Pages the person let your app access.
    curl -G "https://graph.facebook.com/v26.0/me/accounts"   -d "fields=id,name,tasks,access_token"   -d "access_token=<LONG_LIVED_USER_TOKEN>"
    # {"data": [{"id": "<PAGE_ID>", "name": "...", "tasks": ["CREATE_CONTENT", ...],
    #            "access_token": "<PAGE_ACCESS_TOKEN>"}], "paging": {...}}
  5. Let the person choose a Page, then store that Page's ID and token on the server. Meta says token length changes over time, so use a variable-length column with no maximum (access tokens).

For a quick test without writing a login flow, Meta's Graph API Explorer can request the permissions, call /me/accounts, and switch to a Page token from its User or Page menu (getting started). Use it for testing; production tokens should come from your own login flow.

The same flow in TypeScript

This script runs the exchange, pages through /me/accounts until it finds a Page the person can publish to, and inspects that Page's token. It validates every response before using it, reports only the path, HTTP status, and Meta's numeric error code and subcode (never the URL or response body, which can hold tokens), and signs the /me/accounts calls with appsecret_proof.

// Run on your server. The app secret and every token stay there.
import { createHmac } from "node:crypto";

const graph = "https://graph.facebook.com/v26.0";

function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Set ${name}`);
  return value;
}

const appId = requireEnv("META_APP_ID");
const appSecret = requireEnv("META_APP_SECRET");
// The short-lived user token your Facebook Login flow returned.
const userToken = requireEnv("FACEBOOK_USER_TOKEN");

type Json = Record<string, unknown>;
const isJson = (value: unknown): value is Json =>
  typeof value === "object" && value !== null && !Array.isArray(value);

async function get(path: string, params: Record<string, string>): Promise<Json> {
  const url = new URL(graph + path);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
  // Errors name the path and status only. Raw errors and bodies can echo tokens.
  let status = 0;
  let body: unknown;
  try {
    const response = await fetch(url);
    status = response.status;
    body = await response.json();
  } catch {
    throw new Error(`${path} failed: ${status ? `HTTP ${status}, body is not JSON` : "network error"}`);
  }
  if (!isJson(body)) throw new Error(`${path} failed: HTTP ${status}, unexpected response`);
  const error = body["error"];
  if (status !== 200 || isJson(error)) {
    const code = isJson(error) && typeof error["code"] === "number" ? error["code"] : "none";
    const subcode =
      isJson(error) && typeof error["error_subcode"] === "number" ? error["error_subcode"] : "none";
    const detail = `HTTP ${status}, code ${code}, subcode ${subcode}`;
    throw new Error(`${path} failed: ${detail}`);
  }
  return body;
}

// appsecret_proof is an HMAC-SHA256 of the token, keyed with the app secret.
const proof = (token: string) => createHmac("sha256", appSecret).update(token).digest("hex");

// 1. Swap the short-lived user token for a long-lived one (about 60 days).
const exchanged = await get("/oauth/access_token", {
  grant_type: "fb_exchange_token",
  client_id: appId,
  client_secret: appSecret,
  fb_exchange_token: userToken,
});
const longLivedUserToken = exchanged["access_token"];
if (typeof longLivedUserToken !== "string") throw new Error("No long-lived user token");

// 2. List the Pages the user granted, each with its own Page access token,
// and 3. keep the first one this user can publish to. The list is paginated.
type Page = { id: string; name: string; accessToken: string };
let page: Page | undefined;
let after: string | undefined;
do {
  const accounts = await get("/me/accounts", {
    fields: "id,name,tasks,access_token",
    access_token: longLivedUserToken,
    appsecret_proof: proof(longLivedUserToken),
    ...(after ? { after } : {}),
  });
  const rows: unknown[] = Array.isArray(accounts["data"]) ? accounts["data"] : [];
  for (const row of rows) {
    if (!isJson(row)) continue;
    const { id, name, access_token: accessToken, tasks } = row;
    const taskList: unknown[] = Array.isArray(tasks) ? tasks : [];
    if (typeof id !== "string" || typeof accessToken !== "string") continue;
    if (!taskList.includes("CREATE_CONTENT")) continue;
    page = { id, name: typeof name === "string" ? name : id, accessToken };
    break;
  }
  const paging = isJson(accounts["paging"]) ? accounts["paging"] : {};
  const cursors = isJson(paging["cursors"]) ? paging["cursors"] : {};
  const next = cursors["after"];
  after = !page && typeof paging["next"] === "string" && typeof next === "string" ? next : undefined;
} while (after);
if (!page) throw new Error("This user can't create content on any Page they granted.");

// 4. Inspect the Page token with an app token (app ID and secret joined by "|").
const debug = await get("/debug_token", {
  input_token: page.accessToken,
  access_token: `${appId}|${appSecret}`,
});
const info = isJson(debug["data"]) ? debug["data"] : {};
console.log(page.name, page.id, {
  type: info["type"],
  isValid: info["is_valid"],
  expiresAt: info["expires_at"],
  scopes: info["scopes"],
});
// Save page.id and page.accessToken in your secret store. Don't log the token.

We type-checked it with strict TypeScript and ran it on 3 November 2026 on Node.js 24 and Bun, with a stub fetch standing in for Meta. It skipped an analyze-only Page on the first page of results, followed the cursor to the second, printed that Page's token type and scopes, and logged no token. A stubbed 502 with a token in its body produced an error that contained no token. Bun and Node.js 24 run the file as it is; on Node.js 22, use a TypeScript runner such as tsx.

Check a token with debug_token

GET /debug_token tells you who a token belongs to, whether it is valid, when it expires, and which permissions it carries. The caller must use an app token, or a developer's user token, for the same app (debug_token reference).

curl -G "https://graph.facebook.com/v26.0/debug_token"   -d "input_token=<PAGE_ACCESS_TOKEN>"   -d "access_token=<APP_ID>|<APP_SECRET>"
# {"data": {"type": "PAGE", "is_valid": true, "expires_at": ..., "scopes": [...]}}

Useful fields: type, is_valid, expires_at, data_access_expires_at (when the app's access to the person's data ends), scopes, and granular_scopes. When a permission was granted for specific Pages, its granular_scopes entry lists their IDs in target_ids; when it applies to all, no targets are shown (debug_token reference, debugging and error handling). If a Page is missing from /me/accounts and target_ids lists other Pages, the person didn't give your app access to it. The Access Token Debugger shows the same data in a browser.

Post to the Page with the token

A text or link post is one call to /{page-id}/feed (getting started). The response is the post ID, and the post's URL is https://www.facebook.com/{post-id}.

curl -X POST "https://graph.facebook.com/v26.0/<PAGE_ID>/feed"   -H "Content-Type: application/json"   -d '{"message": "Hello from the Pages API", "access_token": "<PAGE_ACCESS_TOKEN>"}'
# {"id": "<PAGE_ID>_<POST_ID>"}

To schedule, set published to false and pass scheduled_publish_time between 10 minutes and 30 days ahead. Photos go to /{page-id}/photos, and an app can only edit posts it created (posts).

When a Page token stops working

Meta doesn't tell you when a token becomes invalid; you find out when a call fails (debugging and error handling). Read the error's code and error_subcode (handling errors):

ErrorWhat to do
190 with subcode 463 or 467The token expired or is invalid. Send the person through login again.
190 with subcode 460The person changed their password or logged out. Ask them to log in again.
190 with subcode 458The person removed your app or never authorized it.
Subcode 492The person behind the Page token no longer has a suitable role on the Page.
10 or 200–299A permission is missing or was removed. Request it again.
4, 17, or 341Throttling. Wait, retry, and check your call volume in the App Dashboard.
506Duplicate post. Change the content before trying again.
368Temporarily blocked for a policy violation.

Only the 190 rows and subcode 492 mean the token itself no longer works. A Page token made from a long-lived user token has no expiration date, but those cases still end it, so build a reconnect path instead of treating the token as permanent. Permission errors need a new grant, and throttling, duplicate, and policy errors are about the request, not the token: keep the token and retry or change the request.

Tokens for your own Pages: system users

If you only post to Pages your own business portfolio owns, a system user can replace the login flow. In Business settings, add a system user, assign it your app with Manage app permission and the Pages it needs, then generate a token with the permissions you choose and an expiration preference. System user tokens are made for automated actions without a person logging in, and Meta says they can be long-lived tokens that don't expire with time, though they can still be invalidated (access tokens). Admin system users reach every asset in the portfolio; employee system users only the assets you assign, which is the safer default.

Keeping Page tokens safe

  • Keep the app secret and every user and Page token on your server. Meta's examples pass tokens as query parameters, so keep these URLs out of logs.
  • Add appsecret_proof, an HMAC-SHA256 of the token keyed with your app secret, to server calls, and turn on Require App Secret in the App Dashboard so a stolen token can't be used without it (secure requests).
  • Store only the Page the person picked. Every Page token in the /me/accounts list is a credential.
  • In a multi-tenant app, check that the signed-in tenant owns the stored Page before every read and write.

Facebook Pages in Social SDK

Social SDK is an open-source TypeScript library for posting to social platforms from your own backend. Its Facebook adapter is on the main branch and not in the current npm release (0.5.0); the Facebook platform page documents it. discoverFacebookPages lists the Pages a user token can manage, with each Page token and a canPublish flag set when the person has CREATE_CONTENT. The adapter takes one Page ID and token, rejects any other Page, and turns error 190 into a reconnect_required outcome. It doesn't send appsecret_proof, so leave Require App Secret off for an app whose calls go through the adapter.

In 0.5.0 you can already post to Instagram, X, LinkedIn, Bluesky, Threads, TikTok, and YouTube (see the supported platforms), or build without any credentials using the mock quickstart.

Common questions

Does a Facebook Page access token expire?
One made from a short-lived user token expires within hours. One made from a long-lived user token has no expiration date, but it stops working if the person changes their password, loses their role on the Page, or removes your app.
How do I get a permanent Facebook Page access token?
Exchange the user token for a long-lived one with fb_exchange_token, then call /me/accounts with it. For Pages your own business owns, a system user token is the other option. Neither is guaranteed forever, so handle error 190.
Why does /me/accounts return an empty list?
The token lacks pages_show_list, the person didn't give your app access to any Page when they logged in, or they have no task on the Page. Check scopes and granular_scopes with debug_token.
Can I post as a Page with a user access token?
No. Get the Page access token from /me/accounts and use that for /{page-id}/feed.
Is the Facebook access token the same as the app secret?
No. The app secret identifies your app and must stay on your server. An app access token can be the app ID and secret joined by |, which is why it is server-only too.

Sources

Meta's pages, read on 3 November 2026, with the update date each page showed: