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):
| Token | What it is for |
|---|---|
| User access token | Acting for a person who logged in to your app and granted permissions. You need one to get a Page token. |
| Page access token | Reading and writing a Page's data: posts, comments, Insights. Unique to each Page, user, and app combination. |
| System user access token | Automated actions on Pages or ad objects owned by your own business portfolio, without a person logging in. |
| App access token | App settings and server calls such as debug_token. Made from your app ID and secret. |
| Client token | Identifying 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 with | You get |
|---|---|
| Web login | A short-lived user token |
| A short-lived user token | A short-lived Page token from /me/accounts |
| A short-lived user token plus your app secret | A long-lived user token (about 60 days) from fb_exchange_token |
| A long-lived user token | A 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.
| Permission | What it unlocks |
|---|---|
pages_show_list | The list of Pages a person manages, which is what /me/accounts returns |
pages_manage_posts | Creating, editing, and deleting Page posts, photos, and videos. Depends on pages_read_engagement and pages_show_list |
pages_read_engagement | Reading the Page's content, followers, and metadata |
read_insights | Reading Page Insights data. Depends on pages_read_engagement and pages_show_list |
pages_manage_engagement | Creating, editing, and deleting the Page's own comments, such as replies, and likes on Page posts |
pages_read_user_content | Reading comments and posts that people and other Pages leave on the Page, and deleting user comments |
publish_video | Publishing 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
- 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.
- Send the person through the login dialog. Your callback gets a short-lived user token.
-
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} -
Call
/me/accountswith 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": {...}} - 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):
| Error | What to do |
|---|---|
190 with subcode 463 or 467 | The token expired or is invalid. Send the person through login again. |
190 with subcode 460 | The person changed their password or logged out. Ask them to log in again. |
190 with subcode 458 | The person removed your app or never authorized it. |
Subcode 492 | The person behind the Page token no longer has a suitable role on the Page. |
10 or 200–299 | A permission is missing or was removed. Request it again. |
4, 17, or 341 | Throttling. Wait, retry, and check your call volume in the App Dashboard. |
506 | Duplicate post. Change the content before trying again. |
368 | Temporarily 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/accountslist 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/accountswith 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. Checkscopesandgranular_scopeswithdebug_token. - Can I post as a Page with a user access token?
- No. Get the Page access token from
/me/accountsand 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:
- Access tokens for Meta technologies (updated 25 November 2025): token types, lifetimes, system users, token length
- Long-lived access tokens (updated 30 June 2026):
fb_exchange_token, long-lived Page tokens, Graph API v26.0 - Get started with the Pages API (updated 30 June 2026):
/me/accounts, posting, Graph API Explorer - Pages API posts (updated 17 April 2026): permissions, scheduling, photos, updates
- Pages API overview (updated 5 May 2025): tasks, App Review, Facebook Login for Business
- Permissions reference (updated 19 October 2026):
pages_manage_posts,pages_read_engagement,pages_show_list,pages_manage_engagement,pages_read_user_content,read_insights - debug_token reference (Graph API v26.0) and debugging and error handling (no date shown): fields and expired-token errors
- Handling errors and secure requests (no date shown): error codes,
appsecret_proof