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
- 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.
- Open Webhook Settings: Click the Configure button next to the Webhook checkbox. This opens the Options page at
#/webhook. - Enter Your Endpoint URL: Fill in your webhook URL (must start with
https://and resolve to a public host — see URL requirements). - Choose Authentication Mode: Select an auth mode if your endpoint requires authentication (see Authentication Modes).
- Save: Click Save. The browser will ask you to grant permission to access the target domain — this is a Chrome extension requirement.
- 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 Type | Blocked Examples | Reason |
|---|---|---|
| Non-HTTPS | http://example.com | Plain HTTP is refused. Only https:// is allowed. |
| Invalid characters | URLs containing line breaks or control characters | Prevents header injection attacks. |
| Private hostnames | localhost, *.local, *.internal, *.localdomain | Private network domains. |
| Cloud metadata | metadata.google.internal, metadata, instance-data | Prevents SSRF attacks on cloud metadata APIs. |
| Private IPv4 | 0.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 equivalents | Private 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
| Mode | Behaviour |
|---|---|
| None | No 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. |
| Header | A 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.
| Event | Trigger | Data Payload |
|---|---|---|
export.start | The user clicks Start — fired once at the very beginning of an export. | collectionId, pageType, fetchLimit |
posts.batch | Fired 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.batch | Fired 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.completed | Fired 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.completed | Fired 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.completed | All posts have been fetched and the export ended naturally. | totalPosts |
export.stopped | The user clicked Stop before the export finished. | totalPosts |
export.failed | A GraphQL request failed or returned an empty response (API rate limit, network error, etc.). | totalPosts, error (error message) |
test | Sent 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 only —contentis untranslated,commentsis an empty array,attachmentsDetailsis 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. Theattachments[].hreffield 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:
{
"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
| Field | Type | Description |
|---|---|---|
extension | string | Human-readable extension name. Always "ESUIT | Posts Exporter for Facebook™". |
extensionId | string | Internal project identifier. Always "export-posts-for-facebook". |
version | string | Extension version string at the time the event fired. |
requestId | string | UUID v4. Unique per request. Use this for idempotency (see Idempotency hint). |
timestamp | number | Unix time in milliseconds when the event was created. |
sourceUrl | string | The 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/"). |
event | string | One of the event names listed in the table above. |
data | object | Event-specific payload (see below). |
auth | object | Only 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:
| Field | Type | Description |
|---|---|---|
post_id | string | Facebook post ID (numeric or alphanumeric). |
feedbackId | string | Facebook internal Feedback ID for the comment section. |
content | string | Post text content. In posts.batch, this is untranslated. In post.completed, this includes translation if enabled. |
url | string | Direct URL to the post on Facebook. |
author.name | string | Author's display name. |
author.id | string | Author's Facebook user ID. |
author.url | string | Author's Facebook profile URL. |
createdAt | number | Unix time in seconds when the post was created. |
comments | Comment[] | Array of comment objects. Empty in posts.batch, fully populated in post.completed. |
attachmentsDetails | AttachmentDetail[] | 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)
| Field | Type | Description |
|---|---|---|
id | string | Relay node ID of the comment. |
commentId | string | Same as id. |
feedbackId | string | Relay Feedback ID of this comment's feedback node. |
content | string | Comment text. Empty string for sticker-only or GIF-only comments. |
url | string | Direct URL to the comment on Facebook. |
author.name | string | Author's display name. |
author.id | string | Author's Facebook user ID. |
author.url | string | Author's Facebook profile URL. |
author.avatar | string | Profile picture CDN URL. May expire — do not cache long-term. |
reactionsCount | number | Total reactions on this comment. |
subCommentsCount | number | Total number of replies this comment has. |
createdAt | number | Unix time in seconds when the comment was posted. |
parentFeedbackId | string | Feedback ID of the parent comment. Empty string for top-level comments. |
depth | number | Nesting depth. 0 = top-level comment, 1 = first reply, and so on. |
children | Comment[] | Nested reply comments. Empty in post.comments.batch, fully populated in post.completed if nested comments are enabled. |
AttachmentDetail Object (in post.attachments.completed)
| Field | Type | Description |
|---|---|---|
id | string | Facebook attachment ID. |
href | string | Facebook 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. |
caption | string | Attachment caption text (if any). |
accessibilityCaption | string | Auto-generated accessibility description (if available). |
createdAt | number | Unix 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
{
"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)
{
"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
{
"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)
{
"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)
{
"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
{
"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.
{
"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).
{
"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
{
"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:
{
"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.tokenfield 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 aPayload exceeds N bytesentry 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 astatusCode: nullentry — 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/*.internalhost. 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 ofhttps://.
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:
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.
