Skip to content

How to use the Webhook feature

The Webhook feature lets you receive real-time notifications as the extension exports posts, comments, and attachments. Each time a batch of posts is fetched, comments are collected, attachments are downloaded, or a post is fully processed — 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 4.0.0 and above. A subscription is required to use this feature (permission level 10).

What is Webhook

A webhook is an HTTP callback that allows the extension to push data to your server in real time as events happen during an export. Instead of polling for updates or manually downloading files, your CRM, database, or automation workflow receives structured JSON payloads immediately as each post, comment batch, or attachment set is processed.

Prerequisites

  • Subscription required: The Webhook feature requires an active subscription with permission level 10 or higher.
  • HTTPS endpoint: Your server must expose a public HTTPS URL. Private networks, localhost, and cloud metadata addresses are blocked (see URL requirements).
  • Optional authentication: You can secure your endpoint with a token (body field or HTTP header).

Configure

  1. Enable Webhook in Export Modal: Open any Facebook page, group, profile, or saved collection. Click the extension icon to open the export modal, and check the Webhook checkbox. If you are not subscribed, checking this checkbox will prompt you to upgrade.
  2. Open Webhook Settings: Click the Configure button next to the Webhook checkbox. This opens the Options page at #/webhook.
  3. Enter Your Endpoint URL: Fill in your webhook URL (must start with https:// and resolve to a public host — see URL requirements).
  4. Choose Authentication Mode: Select an auth mode if your endpoint requires authentication (see Authentication Modes).
  5. Save: Click Save. The browser will ask you to grant permission to access the target domain — this is a Chrome extension requirement.
  6. Send Test: Click Send Test to verify the endpoint receives the request. A test event ("event": "test") will be sent to your endpoint immediately.

Once configured, any export with the Webhook checkbox enabled will send events to your endpoint in real time.

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:

Address TypeBlocked ExamplesReason
Non-HTTPShttp://example.comPlain HTTP is refused. Only https:// is allowed.
Invalid charactersURLs containing line breaks or control charactersPrevents header injection attacks.
Private hostnameslocalhost, *.local, *.internal, *.localdomainPrivate network domains.
Cloud metadatametadata.google.internal, metadata, instance-dataPrevents SSRF attacks on cloud metadata APIs.
Private IPv40.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16 (link-local & 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)Private IP ranges.
Private IPv6::1 (localhost), fe80::/10 (link-local), fc00::/7 (unique-local), IPv4-mapped equivalentsPrivate IPv6 ranges.

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.

Authentication Modes

ModeBehaviour
NoneNo authentication header or field is added. Use this for public endpoints or when your endpoint authenticates by IP whitelist.
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. The token field name is fixed and cannot be renamed.
HeaderA custom HTTP header is added to every request. You specify both the header name (e.g., Authorization) and value (e.g., Bearer sk-1234...). Use this for standard Authorization header 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.

EventTriggerData Payload
export.startThe user clicks Start — fired once at the very beginning of an export.collectionId, pageType, fetchLimit
posts.batchFired after each page of posts is successfully fetched. Contains metadata-stage StoryItems (no translation applied, no attachments processed yet). comments and attachmentsDetails arrays are empty at this stage. May fire many times per export.collectionId, cursor, posts (array of StoryItem metadata)
post.comments.batchFired after each page of comments is fetched for a specific post. May fire many times per post if the post has multiple pages of comments.postId, feedbackId, cursor, comments (array of comment objects)
post.attachments.completedFired after all attachments for a specific post have been downloaded. href fields are Facebook CDN URLs which may expire in a short time — your downstream server should download them immediately.postId, feedbackId, attachments (array of attachment references with id, href, caption, accessibilityCaption, createdAt)
post.completedFired after a specific post is fully processed — translation applied, comments fetched, attachments downloaded, HTML/JSON exported (if enabled). Contains the final, fully processed StoryItem with all nested comments, attachment details, and translated content.postId, post (fully processed StoryItem)
export.completedAll posts have been fetched and the export ended naturally.totalPosts
export.stoppedThe user clicked Stop before the export finished.totalPosts
export.failedA GraphQL request failed or returned an empty response (API rate limit, network error, etc.).totalPosts, error (error message)
testSent when the user clicks Send Test in the Webhook Settings page.note: "Send Test from options page"

Key Differences Between Events

  • posts.batch: Fires immediately after posts are fetched from Facebook's API. StoryItems contain metadata onlycontent is untranslated, comments is an empty array, attachmentsDetails is an empty array. Use this for lightweight indexing or early filtering.
  • post.completed: Fires after all processing is done for a single post — translation, comments, attachments, HTML/JSON generation. The StoryItem is fully populated. Use this for downstream storage or display.
  • post.attachments.completed: Fires after all attachments for a post are downloaded. The attachments[].href field contains the Facebook CDN URL — these URLs may expire after a short time (minutes to hours), so your downstream server should download them immediately upon receiving this event.

Payload Schema

Every request body shares the same top-level envelope:

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

Envelope Fields

FieldTypeDescription
extensionstringHuman-readable extension name. Always "ESUIT | Posts Exporter for Facebook™".
extensionIdstringInternal project identifier. Always "export-posts-for-facebook".
versionstringExtension version string at the time the event fired.
requestIdstringUUID v4. Unique per request. Use this for idempotency (see Idempotency hint).
timestampnumberUnix time in milliseconds when the event was created.
sourceUrlstringThe window.location.href of the Facebook page where the export was triggered. Use this to identify which group, profile, or page the data originates from (e.g. "https://www.facebook.com/groups/1234567890/").
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.

StoryItem Schema (in post.completed and posts.batch)

The posts.batch event contains an array of StoryItem objects in data.posts, and post.completed contains a single StoryItem in data.post. Key fields include:

FieldTypeDescription
post_idstringFacebook post ID (numeric or alphanumeric).
feedbackIdstringFacebook internal Feedback ID for the comment section.
contentstringPost text content. In posts.batch, this is untranslated. In post.completed, this includes translation if enabled.
urlstringDirect URL to the post on Facebook.
author.namestringAuthor's display name.
author.idstringAuthor's Facebook user ID.
author.urlstringAuthor's Facebook profile URL.
createdAtnumberUnix time in seconds when the post was created.
commentsComment[]Array of comment objects. Empty in posts.batch, fully populated in post.completed.
attachmentsDetailsAttachmentDetail[]Array of attachment objects. Empty in posts.batch, fully populated in post.completed.

For a complete StoryItem field reference, see the extension's postDataParser.ts source.

Comment Object (in post.comments.batch and post.completed)

FieldTypeDescription
idstringRelay node ID of the comment.
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.
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.
childrenComment[]Nested reply comments. Empty in post.comments.batch, fully populated in post.completed if nested comments are enabled.

AttachmentDetail Object (in post.attachments.completed)

FieldTypeDescription
idstringFacebook attachment ID.
hrefstringFacebook CDN URL. This URL points to the image or video on Facebook's servers. These URLs may expire after a short time (minutes to hours) — your downstream server should download the file immediately upon receiving this event. Do not rely on these URLs for long-term storage.
captionstringAttachment caption text (if any).
accessibilityCaptionstringAuto-generated accessibility description (if available).
createdAtnumberUnix time in seconds when the attachment was created.

CDN URL Expiration

The href field in post.attachments.completed contains a Facebook CDN URL that may expire in a short time (minutes to hours). Your downstream server must download the file immediately upon receiving this event. Do not cache the URL for later use — it will likely return a 403 or 404 error if accessed after expiration.

Example Payloads

export.start

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "a1b2c3d4-0000-0000-0000-000000000001",
  "timestamp": 1778029800000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "export.start",
  "data": {
    "collectionId": "1234567890",
    "pageType": "GroupsPosts",
    "fetchLimit": 100
  }
}

posts.batch (metadata-stage, pre-translation)

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "61b3f60d-146d-4809-ba83-fe094b5e13dd",
  "timestamp": 1778029854757,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "posts.batch",
  "data": {
    "collectionId": "1234567890",
    "cursor": "MToxNzc4MDI5OD...",
    "posts": [
      {
        "post_id": "123456789012345",
        "storyId": "UzpfSXNhbXBsZXVzZXJpZDpWSzoxMjM0NTY3ODkwMTIzNDU=",
        "feedbackId": "ZmVlZGJhY2s6MTIzNDU2Nzg5MDEyMzQ1",
        "content": "This is a sample post content (untranslated at this stage)",
        "url": "https://www.facebook.com/groups/1234567890/permalink/123456789012345/",
        "author": {
          "name": "John Doe",
          "id": "100012345678901",
          "avatar": "https://scontent-ord5-2.xx.fbcdn.net/...",
          "profile": "https://www.facebook.com/profile.php?id=100012345678901"
        },
        "reactionsCount": 9,
        "shareCount": 1,
        "commentCount": 7,
        "createdAt": 1778130124,
        "comments": [],
        "attachmentsDetails": []
      }
    ]
  }
}

post.comments.batch

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "c2d3e4f5-1111-1111-1111-111111111111",
  "timestamp": 1778029900000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "post.comments.batch",
  "data": {
    "postId": "123456789012345",
    "feedbackId": "ZmVlZGJhY2s6MTIzNDU2Nzg5MDEyMzQ1",
    "cursor": "MToxNzc4Mzc5NDM4OgF_9k_ZUYsJixTuPFUi...",
    "comments": [
      {
        "id": "Y29tbWVudDoxMjM0NTY3ODkwMTIzNDVfMTAwMDAwMDAwMDAwMDE=",
        "commentId": "Y29tbWVudDoxMjM0NTY3ODkwMTIzNDVfMTAwMDAwMDAwMDAwMDE=",
        "feedbackId": "ZmVlZGJhY2s6MTIzNDU2Nzg5MDEyMzQ1XzEwMDAwMDAwMDAwMDEx",
        "content": "🙏❤️ Amen",
        "url": "https://www.facebook.com/groups/1234567890/posts/123456789012345/?comment_id=100000000000001",
        "author": {
          "name": "Jane Smith",
          "id": "9876543210",
          "avatar": "https://scontent-ord5-2.xx.fbcdn.net/...",
          "profile": "https://www.facebook.com/profile.php?id=9876543210"
        },
        "reactionsCount": 0,
        "subCommentsCount": 0,
        "createdAt": 1778130734,
        "parentFeedbackId": "",
        "depth": 0,
        "children": []
      }
    ]
  }
}

post.attachments.completed (with CDN URLs)

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "d3e4f5g6-2222-2222-2222-222222222222",
  "timestamp": 1778030000000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "post.attachments.completed",
  "data": {
    "postId": "123456789012345",
    "feedbackId": "ZmVlZGJhY2s6MTIzNDU2Nzg5MDEyMzQ1",
    "attachments": [
      {
        "id": "111222333444555",
        "href": "https://scontent-ord5-2.xx.fbcdn.net/v/t39.30808-6/sample_image_20260507.jpg?...",
        "accessibilityCaption": "May be an image of text",
        "createdAt": 1778130118
      }
    ]
  }
}

WARNING

The href field contains a Facebook CDN URL that may expire in a short time. Your downstream server should download the file immediately upon receiving this event.

post.completed (fully processed)

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "e4f5g6h7-3333-3333-3333-333333333333",
  "timestamp": 1778030100000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "post.completed",
  "data": {
    "postId": "123456789012345",
    "post": {
      "post_id": "123456789012345",
      "storyId": "UzpfSXNhbXBsZXVzZXJpZDpWSzoxMjM0NTY3ODkwMTIzNDU=",
      "feedbackId": "ZmVlZGJhY2s6MTIzNDU2Nzg5MDEyMzQ1",
      "content": "Post content here (translated if the Translate Content option is enabled)",
      "url": "https://www.facebook.com/groups/1234567890/permalink/123456789012345/",
      "author": {
        "name": "John Doe",
        "id": "100012345678901",
        "avatar": "https://scontent-ord5-2.xx.fbcdn.net/...",
        "profile": "https://www.facebook.com/profile.php?id=100012345678901"
      },
      "reactionsCount": 9,
      "shareCount": 1,
      "commentCount": 7,
      "createdAt": 1778130124,
      "comments": [
        {
          "id": "Y29tbWVudDoxMjM0NTY3ODkwMTIzNDVfMTAwMDAwMDAwMDAwMDE=",
          "commentId": "Y29tbWVudDoxMjM0NTY3ODkwMTIzNDVfMTAwMDAwMDAwMDAwMDE=",
          "feedbackId": "ZmVlZGJhY2s6MTIzNDU2Nzg5MDEyMzQ1XzEwMDAwMDAwMDAwMDEx",
          "content": "🙏❤️ Amen",
          "url": "https://www.facebook.com/groups/1234567890/posts/123456789012345/?comment_id=100000000000001",
          "author": {
            "name": "Jane Smith",
            "id": "9876543210",
            "avatar": "https://scontent-ord5-2.xx.fbcdn.net/...",
            "profile": "https://www.facebook.com/profile.php?id=9876543210"
          },
          "reactionsCount": 0,
          "subCommentsCount": 0,
          "createdAt": 1778130734,
          "parentFeedbackId": "",
          "depth": 0,
          "children": []
        }
      ],
      "attachmentsDetails": [
        {
          "id": "111222333444555",
          "href": "https://scontent-ord5-2.xx.fbcdn.net/v/t39.30808-6/sample_image_20260507.jpg?...",
          "accessibilityCaption": "May be an image of text",
          "createdAt": 1778130118,
          "localPath": "ESUIT Posts Exporter for Facebook/Group Name/2026_05_07__01_02_04/attachments/image.jpg"
        }
      ]
    }
  }
}

export.completed

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "f5g6h7i8-4444-4444-4444-444444444444",
  "timestamp": 1778030200000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "export.completed",
  "data": {
    "totalPosts": 10
  }
}

export.stopped

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

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "g6h7i8j9-5555-5555-5555-555555555555",
  "timestamp": 1778030300000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "export.stopped",
  "data": {
    "totalPosts": 15
  }
}

export.failed

Same shape as export.completed, with "event": "export.failed" and an additional error field. Fired when a GraphQL request throws an error or returns an empty response (e.g. rate-limited by Facebook).

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "h7i8j9k0-6666-6666-6666-666666666666",
  "timestamp": 1778030400000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "export.failed",
  "data": {
    "totalPosts": 8,
    "error": "GraphQL request failed: rate limited by Facebook"
  }
}

test

json
{
  "extension": "ESUIT | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "i8j9k0l1-7777-7777-7777-777777777777",
  "timestamp": 1778030500000,
  "sourceUrl": "chrome-extension://lcmlfdpaglpfbfmhcebhdkcaolfgnohn/src/pages/options/index.html",
  "event": "test",
  "data": {
    "note": "Send Test from options page"
  }
}

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 | Posts Exporter for Facebook™",
  "extensionId": "export-posts-for-facebook",
  "version": "4.0.0",
  "requestId": "a1b2c3d4-0000-0000-0000-000000000001",
  "timestamp": 1778029800000,
  "sourceUrl": "https://www.facebook.com/groups/1234567890/",
  "event": "export.start",
  "auth": { "token": "your-secret-token" },
  "data": {
    "collectionId": "1234567890",
    "pageType": "GroupsPosts",
    "fetchLimit": 100
  }
}

The token field name is always auth.token and cannot be renamed.

Logs & Retention

Every outgoing webhook request is logged automatically. You can review logs at Webhook Logs (#/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 (daily cleanup runs automatically via chrome.alarms).
  • 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.

TIP

Logs do not store the full request payload — only the event name, status, response snippet, and error message (if any). This prevents logs from accumulating large amounts of user data.

Limits

  • Payload size: A single request body is capped at 5 MB (measured after JSON serialization, including the auth.token field if present). If a batch would exceed this (very large post content, many comments, long reply trees, 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 event. Implement retry/dedup on your endpoint side if you need at-least-once semantics — see Idempotency hint.
  • Concurrency: Requests to the same endpoint are sent one at a time (FIFO queue). This avoids hammering your server when many comments or attachments trigger rapid-fire events. Requests to different endpoints do not block each other.

Troubleshooting

No request received at my endpoint

  • Check that the Webhook checkbox is enabled inside the export modal. Enabling it requires an active subscription.
  • Use Send Test to confirm the endpoint is reachable.
  • Check Webhook Logs (#/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 Comments / 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 posts are fetched too quickly. Disable Full Speed mode to add a delay between requests and reduce the chance of hitting the limit.

Duplicate events received

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 — see Idempotency hint.

Idempotency Hint

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 or manual re-runs), use requestId to deduplicate:

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

The extension itself does not retry, but your downstream infrastructure (load balancers, proxies, etc.) may.

Out of Scope

Take Screenshot tasks do NOT trigger any webhook events in this version.

The screenshot pipeline runs in the Options page and is decoupled from the export pipeline. Screenshot processing uses a separate message flow (downloadPostScreenshot, screenshotTaskDone) that is not integrated with the webhook system. Webhook integration for screenshots may be considered in a future release.

If you need to track screenshot completion, you can poll the Options page state or implement a separate notification channel — but webhook events will not fire for screenshots in version 4.0.0.