Skip to content

How to use the Webhook feature

The Webhook feature lets you receive real-time notifications as the extension fetches comments. Each time a batch of comments is collected — or when the export starts, finishes, stops, or fails — the extension sends an HTTP POST request to your configured endpoint.

TIP

The Webhook feature is available in version 2.8.0 and above. It is free to use.

Configure

  1. Open the extension Options page and navigate to Webhook Settings.
  2. Enter your endpoint URL (must start with https:// and resolve to a public host — see URL requirements).
  3. Choose an Auth Mode if your endpoint requires authentication.
  4. Click Save. The browser will ask you to grant access to the target domain — this is a Chrome extension requirement.
  5. Click Send Test to verify the endpoint receives the request.

To enable sending during an export, check the Webhook checkbox inside the export modal on any Facebook post.

URL requirements

The extension rejects URLs that point to private or internal addresses to prevent accidental data leaks to local services or cloud metadata endpoints. The following are blocked at save time and at send time:

  • Non-https:// URLs (plain http:// is refused).
  • Hostnames containing line breaks or other invalid characters.
  • localhost and any hostname ending in .local, .internal, .localdomain.
  • Cloud metadata aliases such as metadata.google.internal, metadata, instance-data.
  • IPv4 addresses in 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16 (link-local and AWS/GCP metadata 169.254.169.254), 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 (CGNAT), 224.0.0.0/4 (multicast/reserved).
  • IPv6 addresses in ::1, fe80::/10 (link-local), fc00::/7 (unique-local), and IPv4-mapped equivalents.

If your service runs on a private network, expose it through a public HTTPS tunnel (e.g. Cloudflare Tunnel, ngrok, Tailscale Funnel) before pointing the extension at it.

Auth modes

ModeBehaviour
NoneNo authentication header or field is added.
Field (auth.token)Your token is sent in the request body under a fixed auth.token field (see envelope). Use this for token-in-body style APIs.
HeaderA custom HTTP header is added to every request. Use this for Authorization: Bearer … style APIs.

TIP

The Field mode always uses the fixed key auth.token — you cannot rename it. This guarantees the token never collides with any reserved envelope field (event, data, requestId, etc.).

Events

Every request uses Content-Type: application/json and POST method. The event field identifies what happened.

EventTrigger
export.startThe user clicks Start — fired once at the very beginning of an export.
comments.batchFired after each page of comments is successfully fetched. May fire many times per export.
export.completedAll comments have been fetched and the export ended naturally.
export.stoppedThe user clicked Stop before the export finished.
export.failedA GraphQL request failed or returned an empty response (API rate limit, network error, etc.).
testSent when the user clicks Send Test in the Webhook Settings page.

Payload schema

Every request body shares the same top-level envelope:

json
{
  "extension": "ESUIT | Comments Exporter for Facebook™",
  "extensionId": "export-comments-for-facebook",
  "version": "2.8.0",
  "requestId": "61b3f60d-146d-4809-ba83-fe094b5e13dd",
  "timestamp": 1778029854757,
  "event": "<event name>",
  "data": { ... }
}

Envelope fields

FieldTypeDescription
extensionstringHuman-readable extension name.
extensionIdstringInternal project identifier. Always "export-comments-for-facebook".
versionstringExtension version string at the time the event fired.
requestIdstringUUID v4. Unique per request. Use this for idempotency (see below).
timestampnumberUnix time in milliseconds when the event was created.
eventstringOne of the event names listed in the table above.
dataobjectEvent-specific payload (see below).
authobjectOnly present when Auth Mode is Field. Shape: { "token": "<your token>" }. The key auth.token is fixed and cannot be renamed.

data fields

FieldTypePresent inDescription
postUrlstringAll eventsFacebook URL of the post being exported.
postIdstringAll eventsFacebook internal Story ID (base64-encoded Relay ID).
feedbackIdstringAll eventsFacebook internal Feedback ID for the comment section.
cursorstring | nullAll eventsPagination cursor for the next page. Only non-null in comments.batch when more pages follow. null in all other events and on the final batch.
commentsComment[]comments.batchArray of comment objects fetched in this batch. Empty array in all other events.

Comment object

FieldTypeDescription
idstringRelay node ID of the comment (same as commentId).
commentIdstringSame as id.
feedbackIdstringRelay Feedback ID of this comment's feedback node.
contentstringComment text. Empty string for sticker-only or GIF-only comments.
urlstringDirect URL to the comment on Facebook.
author.namestringAuthor's display name.
author.idstringAuthor's Facebook user ID (numeric string).
author.urlstringAuthor's Facebook profile URL.
author.avatarstringProfile picture CDN URL. May expire — do not cache long-term.
reactionsCountnumberTotal reactions on this comment.
subCommentsCountnumberTotal number of replies this comment has.
createdAtnumberUnix time in seconds when the comment was posted.
parentFeedbackIdstringFeedback ID of the parent comment. Empty string for top-level comments.
depthnumberNesting depth. 0 = top-level comment, 1 = first reply, and so on.
children[]Always an empty array in webhook payloads. Nested replies are delivered as separate comments.batch events.

Example payloads

export.start

json
{
  "extension": "ESUIT | Comments Exporter for Facebook™",
  "extensionId": "export-comments-for-facebook",
  "version": "2.8.0",
  "requestId": "a1b2c3d4-0000-0000-0000-000000000001",
  "timestamp": 1778029800000,
  "event": "export.start",
  "data": {
    "postUrl": "https://www.facebook.com/reel/26873536312240948/",
    "postId": "UzpfSTEwMDA1OTM5NjE0NzE0ODpWSzoyNjg3MzUzNjMxMjI0MDk0OA==",
    "feedbackId": "ZmVlZGJhY2s6MTM4NDY2MDM1MDE5MDQ5MA==",
    "cursor": null,
    "comments": []
  }
}

comments.batch

cursor is non-null when more pages remain; null on the last batch.

json
{
  "extension": "ESUIT | Comments Exporter for Facebook™",
  "extensionId": "export-comments-for-facebook",
  "version": "2.8.0",
  "requestId": "61b3f60d-146d-4809-ba83-fe094b5e13dd",
  "timestamp": 1778029854757,
  "event": "comments.batch",
  "data": {
    "postUrl": "https://www.facebook.com/reel/26873536312240948/",
    "postId": "UzpfSTEwMDA1OTM5NjE0NzE0ODpWSzoyNjg3MzUzNjMxMjI0MDk0OA==",
    "feedbackId": "ZmVlZGJhY2s6MTM4NDY2MDM1MDE5MDQ5MA==",
    "cursor": "MToxNzc4MDI5OD...",
    "comments": [
      {
        "id": "Y29tbWVudDoxMzg0NjYwMzUwMTkwNDkwXzIwMTA2NTI4NDY0ODA5NDU=",
        "feedbackId": "ZmVlZGJhY2s6MTM4NDY2MDM1MDE5MDQ5MF8yMDEwNjUyODQ2NDgwOTQ1",
        "commentId": "Y29tbWVudDoxMzg0NjYwMzUwMTkwNDkwXzIwMTA2NTI4NDY0ODA5NDU=",
        "content": "He stole my old man dance",
        "url": "https://www.facebook.com/reel/26873536312240948/?comment_id=2010652846480945",
        "author": {
          "name": "Gregory W Sullivan",
          "id": "1797916844",
          "url": "https://www.facebook.com/gregory.w.sullivan.5",
          "avatar": "https://scontent-ord5-2.xx.fbcdn.net/..."
        },
        "reactionsCount": 37,
        "subCommentsCount": 3,
        "createdAt": 1778005662,
        "parentFeedbackId": "",
        "depth": 0,
        "children": []
      }
    ]
  }
}

export.completed

json
{
  "extension": "ESUIT | Comments Exporter for Facebook™",
  "extensionId": "export-comments-for-facebook",
  "version": "2.8.0",
  "requestId": "a1b2c3d4-0000-0000-0000-000000000099",
  "timestamp": 1778030100000,
  "event": "export.completed",
  "data": {
    "postUrl": "https://www.facebook.com/reel/26873536312240948/",
    "postId": "UzpfSTEwMDA1OTM5NjE0NzE0ODpWSzoyNjg3MzUzNjMxMjI0MDk0OA==",
    "feedbackId": "ZmVlZGJhY2s6MTM4NDY2MDM1MDE5MDQ5MA==",
    "cursor": null,
    "comments": []
  }
}

export.stopped

Same shape as export.completed, with "event": "export.stopped". Fired when the user clicks Stop before all pages are fetched.

export.failed

Same shape as export.completed, with "event": "export.failed". Fired when a GraphQL request throws an error or returns an empty response (e.g. rate-limited by Facebook). The extension always emits exactly one terminal event (export.completed, export.stopped, or export.failed) per export run, so downstream batch handlers can rely on it to close out a batch.

Auth mode Field — body with auth.token

When Auth Mode is set to Field, every request gains a top-level auth object alongside the usual envelope:

json
{
  "extension": "ESUIT | Comments Exporter for Facebook™",
  "extensionId": "export-comments-for-facebook",
  "version": "2.8.0",
  "requestId": "a1b2c3d4-0000-0000-0000-000000000001",
  "timestamp": 1778029800000,
  "event": "export.start",
  "auth": { "token": "your-secret-token" },
  "data": { "...": "..." }
}

Logs & retention

Every outgoing webhook request is logged automatically. You can review logs at Webhook Logs in the Options page.

  • Logs are kept for 7 days.
  • A maximum of 500 log entries are stored. Older entries are pruned first.
  • Each log entry shows the time, event name, HTTP status, duration, and a truncated response body.
  • You can clear all logs at any time with the Clear Logs button.

Limits

  • Payload size: a single request body is capped at 5 MB. If a batch would exceed this (very large reply trees, long comment text, etc.), the extension skips the network call and writes a Payload exceeds N bytes entry into Webhook Logs. Lower the Fetch Count or Nesting Depth to reduce the per-batch size.
  • Retries: none. A failed request is logged once and the export continues with the next batch. Implement retry/dedup on your endpoint side if you need at-least-once semantics — see Idempotency.
  • Concurrency: requests to the same endpoint are sent one at a time (in order). This avoids hammering your server when nested replies trigger many comments.batch events in quick succession.

Troubleshooting

No request received at my endpoint

  • Check that the Webhook checkbox is enabled inside the export modal.
  • Use Send Test to confirm the endpoint is reachable.
  • Check Webhook Logs for a statusCode: null entry — this usually means the URL was unreachable or the domain permission was not granted.

Blocked: … entries in Webhook Logs

The extension refused to send because the URL failed validation. Common causes:

  • The URL points to a private IP / localhost / *.internal host. See URL requirements for the full list.
  • The URL or a header contains line breaks (CR/LF). Re-paste your token without trailing newlines.
  • The URL is http:// instead of https://.

Payload exceeds N bytes

A batch was larger than the 5 MB limit. Reduce Fetch Count in the export modal, or turn off Includes Nesting Comments / lower the Nesting Depth.

CORS errors in the background console

The extension requests host permission for the exact domain you configure. If you change the URL to a different domain, open Webhook Settings and click Save again to trigger the new permission prompt. Send Test also requests permission before firing, so you don't have to save first just to test.

HTTP 4xx or 5xx responses

The extension does not retry failed requests. Your endpoint must return a 2xx status; anything else is logged as an error but the export continues.

export.failed received unexpectedly

Facebook rate-limits GraphQL requests when comments are fetched too quickly. Disable Full Speed mode to add a delay between requests and reduce the chance of hitting the limit.

Idempotency

Each request carries a unique requestId (UUID v4). If your endpoint may receive the same event twice (e.g. due to network retries on your infrastructure), use requestId to deduplicate:

js
if (await db.exists({ requestId: payload.requestId })) return; // already processed
await db.insert(payload);