Skip to content

02 Get your data in /Integrations

Facebook Page comments

Send comments on your Facebook Page posts to Dimension Labs with the universal API.

  1. FP Facebook Page commentsTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

This brings in the posts on your own Facebook Page and every comment and reply on them: each post thread as one conversation, with what the public wrote as customer messages and your Page’s posts and replies as your side. You run it on your side: a small script pulls from Meta’s Graph API and posts each message to the universal API.

  • A Facebook Page your business manages, and a person who can perform the MODERATE or CREATE_CONTENT task on it.
  • A Meta app with Facebook Login for Business, granted pages_read_engagement and pages_read_user_content. Meta’s comments guide also lists pages_manage_engagement and pages_read_user_engagement for working with comments.
  • Standard Access is enough when the people who use the app have a role on it, which covers a brand reading its own Page. Meta says all Business apps are approved for Standard Access automatically. Advanced Access, through App Review, is only needed to serve Pages of people who have no role on your app.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Meta’s steps are in Access levels and Get long-lived tokens.

  1. Create a Business app in the Meta App Dashboard and add Facebook Login for Business.
  2. Sign in through the app as a person with a role on the Page, granting the permissions above, and exchange the short-lived User access token for a long-lived one.
  3. Get the Page access token: GET https://graph.facebook.com/v26.0/{app-scoped-user-id}/accounts?access_token={long-lived-user-access-token}. Each Page in the answer has its id and access_token. Meta says a long-lived Page access token made this way has no expiration date, though it can be invalidated (for example if the person loses their role on the Page). Keep it in a secrets manager.
  4. Meta puts the Graph API version in the path. This guide uses v26.0, the version Meta’s reference showed when it was checked. Pin a version and move it forward on purpose.
Terminal window
curl -G "https://graph.facebook.com/v26.0/PAGE_ID/feed" \
--data-urlencode "fields=id,message,created_time,from" \
--data-urlencode "since=1759276800" --data-urlencode "until=1761955200" \
--data-urlencode "limit=100" --data-urlencode "access_token=$FB_PAGE_TOKEN"
curl -G "https://graph.facebook.com/v26.0/PAGE_POST_ID/comments" \
--data-urlencode "filter=stream" \
--data-urlencode "fields=id,message,created_time,from,parent{id}" \
--data-urlencode "access_token=$FB_PAGE_TOKEN"
  • /{page-id}/feed lists the Page’s posts, posts by visitors and posts the Page is tagged in. limit goes up to 100. Meta says the API returns about 600 ranked, published posts per year.
  • Incremental: the feed is time-paginated. since and until take Unix timestamps; Meta recommends sending both, no more than 6 months apart. Meta’s comments reference lists no time filter, so the code reads every comment on recent posts and skips the ones it has already sent.
  • filter=stream returns comments at all levels, replies included, in chronological order. The default, toplevel, leaves replies out.
  • Pagination: follow paging.next until it is missing.
  • A comment by your Page has your Page’s id in from.id, so that is how the code tells your replies from the public’s. Meta returns the commenter in from only with a Page access token.
  • Rate limits (Meta’s): with a Page access token, calls in a rolling 24 hours are capped at 4,800 times the number of people who engaged with the Page. A throttled call returns error code 80001 (or 4, 17, 32 or 613), and the X-Business-Use-Case-Usage header gives estimated_time_to_regain_access in minutes. Meta asks you to stop calling until then.

Storing the data. Meta’s Platform Terms apply to what you pull. They let you use a service provider such as Dimension Labs only under a written agreement that it processes the data for you and for no other purpose. They also ask you to delete Platform Data when you no longer need it for a legitimate business purpose, or when Meta or the person asks. You can’t delete individual records in Dimension Labs yourself: email support@dimensionlabs.io with the source and the comment ids for a targeted deletion.

Each post is one conversation. What the public wrote (visitor posts, comments, replies) is incoming; your Page’s posts and replies are outgoing, in the same session.

Dimension LabsFacebookNotes
textpost message, comment messageSkip comments with no message (a sticker or a photo on its own).
userIdfrom.idThe Page-scoped id of the commenter. Your Page’s own messages use facebook-page-PAGE_ID. Fall back to the sessionId.
incomingfrom.idfalse when from.id is your Page’s id, true otherwise.
sessionIdpost idPrefix it, for example facebook-PAGEID_POSTID. Post ids join two numbers with _, so there are no colons.
conversationIdpost idSame value as sessionId.
dimensionlabs_timestampcreated_timeLike 2020-02-19T23:05:53+0000. Convert with toMillis. A comment within a second of its post is moved a second later.
platformJsonPage id, post id, comment id, parent.id, and whether it is a post, comment or replyparent.id ties a reply to the comment it answers.

Personal data. Don’t send from.name. Comments can hold names, phone numbers and order details: redact them before you send. See PII redaction.

// facebook-comments.mjs. Environment: FB_PAGE_ID, FB_PAGE_TOKEN, DL_API_KEY.
// Optional: FB_GRAPH_VERSION (default v26.0), FB_LOOKBACK_DAYS (default 30).
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000, DAY = 24 * HOUR;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const PAGE = process.env.FB_PAGE_ID;
const API = `https://graph.facebook.com/${process.env.FB_GRAPH_VERSION || 'v26.0'}`;
const THROTTLED = new Set([4, 17, 32, 613, 80001]); // Meta's rate limit error codes
// Minutes Meta says to wait, from the X-Business-Use-Case-Usage header, if it is sent.
function regainMinutes(res) {
try {
const usage = Object.values(JSON.parse(res.headers.get('x-business-use-case-usage') || '{}')).flat();
return Math.max(0, ...usage.map((u) => Number(u.estimated_time_to_regain_access) || 0));
} catch { return 0; }
}
// Retries rate limits (429 or a throttle error code) and server errors (5xx), honoring Retry-After when it is sent.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const u = new URL(url);
if (!u.searchParams.has('access_token')) u.searchParams.set('access_token', process.env.FB_PAGE_TOKEN);
const res = await fetch(u);
const body = await res.json().catch(() => ({}));
if (res.ok) return body;
const throttled = res.status === 429 || THROTTLED.has(body.error?.code);
if ((!throttled && res.status < 500) || attempt === 5) throw new Error(`Facebook returned ${res.status}: ${JSON.stringify(body)}`);
const wait = Number(res.headers.get('retry-after')) || regainMinutes(res) * 60 || 30 * 2 ** attempt;
await sleep(wait * 1000);
}
}
// Follows paging.next until the last page.
async function* pages(url) {
while (url) {
const page = await get(url);
yield page.data ?? [];
url = page.paging?.next;
}
}
const who = (from) => (from?.id === PAGE ? `facebook-page-${PAGE}` : from?.id ? `facebook-${from.id}` : null);
async function send(msg, historical) {
await sendToDimensionLabs(msg, { historical: historical || msg.dimensionlabs_timestamp < Date.now() - HOUR });
}
// Sends every post and comment on the Page created at or after `since` (Unix ms). Returns the new `since`.
export async function syncFacebookComments({ since, historical = false }) {
let newest = since;
const from = historical ? since : since - Number(process.env.FB_LOOKBACK_DAYS || 30) * DAY; // comments arrive on older posts too
const now = Date.now();
// Meta recommends both since and until, at most about 6 months apart, so walk the feed in 90-day windows.
for (let start = from; start < now; start += 90 * DAY) {
const q = new URLSearchParams({ since: String(Math.floor(start / 1000)), until: String(Math.floor(Math.min(start + 90 * DAY, now) / 1000)),
fields: 'id,message,created_time,from', limit: '100' });
for await (const posts of pages(`${API}/${PAGE}/feed?${q}`)) {
for (const post of posts) {
const sessionId = `facebook-${post.id}`;
const postTs = toMillis(post.created_time);
const base = { sessionId, conversationId: sessionId, platformJson: { pageId: PAGE, postId: post.id } };
if (post.message?.trim() && postTs >= since) {
await send({ ...base, text: post.message.trim(), userId: who(post.from) ?? sessionId, incoming: post.from?.id !== PAGE,
dimensionlabs_timestamp: postTs, platformJson: { ...base.platformJson, kind: 'post' } }, historical);
newest = Math.max(newest, postTs);
}
const cq = new URLSearchParams({ filter: 'stream', fields: 'id,message,created_time,from,parent{id}', limit: '100' });
for await (const comments of pages(`${API}/${post.id}/comments?${cq}`)) {
for (const c of comments) {
const created = toMillis(c.created_time);
if (created < since || !c.message?.trim()) continue; // already sent, or a sticker or photo with no text
newest = Math.max(newest, created);
await send({ ...base, text: c.message.trim(), userId: who(c.from) ?? sessionId, incoming: c.from?.id !== PAGE,
dimensionlabs_timestamp: Math.max(created, postTs + 1000),
platformJson: { ...base.platformJson, kind: c.parent ? 'reply' : 'comment', commentId: c.id, parentId: c.parent?.id } }, historical);
}
}
}
}
}
return newest;
}

On every run the code reads the posts from the last 30 days (FB_LOOKBACK_DAYS) and their comments, because people keep commenting on older posts. It sends only what was written since the last run. A new comment on an older post goes to /track and joins that post’s conversation. For a busy Page, a longer lookback costs more calls.

Save the code as facebook-comments.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncFacebookComments as sync } from './facebook-comments.mjs';.

  • History first: node run.mjs 2025-01-01 walks the feed from that date in 90-day windows and sends every post and comment to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new comments to /track, and they are enriched automatically every day. Anything more than an hour old when the job sees it goes to /trackhistorical, and needs a backfill.
  • Re-runs are safe: duplicates are removed automatically.

When Meta throttles the app, the code waits for the time Meta gives and tries again. Meta also offers Webhooks for Pages, which push new comments to a server you host, if polling every 15 minutes uses too many calls. If you have comments in a file instead, upload it and check it first with the file checker, then run a backfill.

Checked against Meta’s documentation on 2026-10-26.