← Back to list

Building a Real-Time Facebook Page Comment Listener Using Webhooks (Node.js + Graph API)

If you’ve tried enabling Facebook Page webhooks in the Meta UI, you probably know the frustration: it looks “subscribed,” but your server…

Hasiniwij in Emojot Engineering · 2026-02-19 06:08 · 0 claps · 3.3 min read
#meta #facebook-api #facebook-graph-api #graph-api #facebook-comments
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Building a Real-Time Facebook Page Comment Listener Using Webhooks (Node.js + Graph API)

If you’ve tried enabling Facebook Page webhooks in the Meta UI, you probably know the frustration: it looks “subscribed,” but your server receives absolutely nothing.

The most reliable approach is to:

  1. Subscribe via the Graph API
  2. Properly verify your webhook endpoint
  3. Handle feed changes (which include comments) in code

In this article, we’ll build a real-time Facebook Page comment listener using:

  • Facebook Graph API (v23.0)
  • Webhooks
  • Node.js + Express

What We’re Building

Webhook → Your Server → Normalize → Store

When something happens on a Facebook Page, Meta sends a webhook payload like this:

field: "feed"
value.item: "comment" | "post" | "photo" | "video" | ...
value.verb: "add" | "edited" | "remove" | ...

Important

You do not subscribe to “comments” directly.

Comments are delivered under:

field: "feed"
item: "comment"

That’s how Facebook structures Page activity.

Step 0: Get Your App ID and App Secret

  1. Visit: https://developers.facebook.com/
  2. Open your app from My Apps.
  3. Navigate to App Settings → Basic

There you’ll see:

  • App ID
  • App Secret (click “Show” to reveal it)

Important: Keep Your App Secret Private

Never:

  • Commit it to GitHub
  • Hardcode it in source code
  • Expose it in frontend JavaScript
  • Log it in production logs

Store it as an environment variable instead:

export FB_APP_SECRET=your_secret_here

Then use it in Node:

process.env.FB_APP_SECRET

Step 1: Subscribe via Graph API (App-level + Page-level)

There are two subscriptions required:

App-level subscription

Tells Meta where to send Page events for your app.

Page-level subscription

Tells a specific Page to send its events to your app.

Even if the UI looks configured, if you skip the Page-level step you’ll receive nothing!

1A) Create an App Access Token

curl -G "https://graph.facebook.com/v23.0/oauth/access_token" \
  --data-urlencode "client_id=APP_ID" \
  --data-urlencode "client_secret=APP_SECRET" \
  --data-urlencode "grant_type=client_credentials"

Response:

{ "access_token": "APP|..." }

1B) Register the App Webhook Subscription

curl -X POST "https://graph.facebook.com/v23.0/APP_ID/subscriptions" \
  -d "object=page" \
  -d "callback_url=https://YOUR_DOMAIN/webhooks/facebook" \
  -d "verify_token=YOUR_VERIFY_TOKEN" \
  -d "fields=feed" \
  -d "access_token=APP_ACCESS_TOKEN"

To verify:

curl "https://graph.facebook.com/v23.0/APP_ID/subscriptions?access_token=APP_ACCESS_TOKEN"

We subscribe to:

object=page
fields=feed

Because feed includes comments.

1C) Subscribe a Page to Your App

You need a Page Access Token for the target Page.

curl -X POST "https://graph.facebook.com/v23.0/PAGE_ID/subscribed_apps" \
  -d "subscribed_fields=['feed']" \
  -d "access_token=PAGE_ACCESS_TOKEN"

Verify:

curl "https://graph.facebook.com/v23.0/PAGE_ID/subscribed_apps?access_token=PAGE_ACCESS_TOKEN"

If the Page is not subscribed, your webhook will never fire.

Step 2: Webhook Verification (GET) — Required for Setup

When configuring webhooks, Meta sends a verification request containing hub.challenge.

Your endpoint must return that challenge.

app.get("/webhooks/facebook", (req, res) => {
    const VERIFY_TOKEN = '1234';
    const mode = req.query['hub.mode'];
    const token = req.query['hub.verify_token'];
    const challenge = req.query['hub.challenge'];

    if (mode && token === VERIFY_TOKEN) {
        res.status(200).send(challenge);
    } else {
        res.sendStatus(403);
    }
}

Step 3: Webhook Receiver (POST) — Parse entry and changes

Facebook batches events under:

body.entry[]
entry.changes[]
change.field
change.value

A production-friendly handler:

app.post("/webhooks/facebook", async (req, res) => {
  try {
    const entries = req.body?.entry || [];
    for (const entry of entries) {
      const pageId = entry.id;
      const changes = entry.changes || [];
      for (const change of changes) {
        const field = change.field;
        const value = change.value || {};
        if (field === "feed") {
          await facebookFeed(value, pageId);
        }
      }
    }
    return res.status(200).send("EVENT_RECEIVED");
  } catch (e) {
    // Always respond 200 to prevent retry storms
    return res.status(200).send("OK");
  }
});

Step 4: Detect Comment Activity

Inside feed, comments are identified by:

const item = value.item; // "post" | "comment" | ...
const verb = value.verb; // "add" | "edited" | "remove"

To handle comments:

if (item === "comment") {
   // process comment
}

Build a Human-Friendly Link

For dashboards or alerts:

link = `https://www.facebook.com/${postId}?comment_id=${encodeURIComponent(commentId)}`;

This allows direct navigation to the comment.

Step 5: Enrich Comment Events

Webhook payloads often don’t include media or parent comment info.

To enrich:

GET /{commentId}?fields=attachment,parent

Example:

const url = `https://graph.facebook.com/v23.0/${commentId}`;
const { data } = await axios.get(url, {
  params: {
    fields: "attachment,parent",
    access_token: pageAccessToken,
  },
});

Extract useful data:

  • data.parent.id → identifies threaded replies
  • data.attachment.media.image.src
  • data.attachment.media.animated_image.uri
  • data.attachment.media.source

This allows you to:

  • Display media previews
  • Detect reply threads
  • Normalize comment structure

Step 6: Normalize into a Unified Object

Unifying all feed events into a single structure simplifies storage and downstream processing.

Example:

const feedObj = {
  accountId: pageId,
  channel: "facebook",
  sourceField: "feed",
  item: item || "post",
  verb: verb || null,
  uu_id: commentIdOrPostId,
  text,
  link,
  profileName,
  profileUrl,
  imageUrl,
  mediaType,
  mention_parent_id,
  dateTime: createdAt,
};

Handling Edits & Deletions

Two practical rules:

  • If verb === "edited" and there is no text or image → treat as deleted
  • If verb === "remove" and item === "comment" → mark deleted

These small details prevent UI inconsistencies later.

Final Thoughts

To reliably build a Facebook comment listener:

  • Subscribe the app via /APP_ID/subscriptions
  • Subscribe the Page via /PAGE_ID/subscribed_apps
  • Listen for field="feed"
  • Detect item="comment"
  • Enrich with fields=attachment,parent
  • Normalize and store

That’s your real-time Facebook comment ingestion pipeline.

What’s Next?

In Part 2, we’ll cover:

  • Replying to comments using POST /{id}/comments

메타데이터
post_id
17dad90e992e
slug
building-a-real-time-facebook-page-comment-listener-using-webhooks-node-js-graph-api-17dad90e992e
url
https://medium.com/emojot-engineering/building-a-real-time-facebook-page-comment-listener-using-webhooks-node-js-graph-api-17dad90e992e
canonical_url
https://medium.com/emojot-engineering/building-a-real-time-facebook-page-comment-listener-using-webhooks-node-js-graph-api-17dad90e992e
author_url
https://medium.com/@hasiniwij
status
ok
fetched_at
2026-07-19 21:53:32