# Facebook Page comments

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

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](/api/tracker).

## What you need

- 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](/api/tracker#your-api-key).

## 1. Get API access in Facebook

Meta's steps are in [Access levels](https://developers.facebook.com/docs/graph-api/overview/access-levels/) and [Get long-lived tokens](https://developers.facebook.com/docs/facebook-login/guides/access-tokens/get-long-lived/).

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.

## 2. Pull new comments

```bash
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.

## 3. Map the fields

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 Labs | Facebook | Notes |
|---|---|---|
| `text` | post `message`, comment `message` | Skip comments with no `message` (a sticker or a photo on its own). |
| `userId` | `from.id` | The Page-scoped id of the commenter. Your Page's own messages use `facebook-page-PAGE_ID`. Fall back to the `sessionId`. |
| `incoming` | `from.id` | `false` when `from.id` is your Page's id, `true` otherwise. |
| `sessionId` | post `id` | Prefix it, for example `facebook-PAGEID_POSTID`. Post ids join two numbers with `_`, so there are no colons. |
| `conversationId` | post `id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `created_time` | Like `2020-02-19T23:05:53+0000`. Convert with `toMillis`. A comment within a second of its post is moved a second later. |
| `platformJson` | Page id, post id, comment `id`, `parent.id`, and whether it is a post, comment or reply | `parent.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](/data/pii-redaction).

## 4. Send it

```js
// 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).


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.

## 5. Run it

Save the code as `facebook-comments.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/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](/dimensions/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](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill.

## Facebook reference

- [Comments and @mentions (Pages API)](https://developers.facebook.com/docs/pages-api/comments-mentions/)
- [/{object-id}/comments](https://developers.facebook.com/docs/graph-api/reference/object/comments/)
- [Comment](https://developers.facebook.com/docs/graph-api/reference/comment/)
- [Page feed](https://developers.facebook.com/docs/graph-api/reference/page/feed/)
- [Paginated results](https://developers.facebook.com/docs/graph-api/results)
- [Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting/)
- [Access levels](https://developers.facebook.com/docs/graph-api/overview/access-levels/)
- [Get long-lived tokens](https://developers.facebook.com/docs/facebook-login/guides/access-tokens/get-long-lived/)
- [Meta Platform Terms](https://developers.facebook.com/terms/)

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