Webhooks
Webhooks allow your application to receive real-time HTTP notifications when events occur in your Postproxy account, such as a post being processed, a platform post being published or failing, or a profile being disconnected.
When an event occurs, Postproxy sends a POST request to your configured URL with a JSON payload describing the event.
Event types
Section titled “Event types”| Event | Description |
|---|---|
post.processed | A post has been processed and is ready for publishing |
post.imported | A post was imported from a connected platform during sync |
platform_post.published | A post was successfully published to a platform |
platform_post.failed | A post failed to publish to a platform |
platform_post.failed_waiting_for_retry | A post failed but will be retried |
platform_post.edited | A published post’s text was edited on a platform |
platform_post.edit_failed | Editing a published post on a platform failed |
platform_post.insights | New insights/analytics are available for a platform post |
profile.disconnected | A connected social profile was disconnected |
profile.connected | A new social profile was connected |
profile.stats | A new profile stats snapshot was recorded (fires once per placement) |
profile.placements_synced | A pull of a profile’s placements finished — placements_sync_status is synced or failed. Fires after connect, on the periodic refresh, and on manual syncs |
placement.orphaned | A placement’s serving profile disconnected — the placement and its history remain, but collection and posting pause until a profile with access serves it again |
placement.restored | An orphaned placement is served again — its original profile reconnected, or another connected profile with access took it over |
media.failed | A media attachment failed to process or download |
comment.created | A comment was successfully created on a platform post |
comment.failed | An outgoing comment failed permanently. Payload error / error_details describe the platform error |
comment.failed_waiting_for_retry | An outgoing comment failed and will be retried |
comment.delivery_unconfirmed | An outgoing comment failed in a way that could mean it was posted. It is not sent again — Postproxy checks the platform for it instead. Not terminal; see comment status lifecycle |
comment.edited | A comment edit made through Postproxy was confirmed by the platform. body carries the new text and external_edited_at is set |
comment.edit_failed | The platform rejected a comment edit. body is unchanged; error / error_details describe the rejection |
profile_comment.created | A profile-scoped comment appeared (a new Google Business review, a Facebook Page recommendation, or a published reply). Fires for google_business and facebook profiles, once per new review across all locations / Pages of the account; filter on data.object.placement_id if you only care about a subset. |
profile_comment.failed | An outgoing profile-comment reply failed permanently. Payload error / error_details describe the platform error |
profile_comment.failed_waiting_for_retry | An outgoing profile-comment reply failed and will be retried |
profile_comment.deleted | A previously synced Google Business review or owner reply no longer exists on Google (removed by the reviewer, by Google moderation, or by the owner). The row stays fetchable by id with status: "deleted" but drops out of list responses. google_business only |
message.received | An inbound direct message arrived |
message.sent | An outbound direct message was accepted by the platform |
message.delivered | The platform confirmed delivery to the recipient (WhatsApp, Facebook & Instagram) |
message.read | The recipient read the message (WhatsApp, Facebook & Instagram) |
message.edited | A message was edited on the platform |
message.deleted | A message was deleted/unsent on the platform (Instagram and WhatsApp) |
message.failed_waiting_for_retry | An outbound message failed and will be retried. Payload error_message / error_details describe the platform error |
message.failed | An outbound message failed permanently. Payload error_message / error_details describe the platform error |
reaction.received | A reaction was added or removed on a message (WhatsApp, Facebook & Instagram) |
referral.received | A participant entered a DM thread via an ig.me / m.me / wa.me link, or a Click-to-WhatsApp ad (WhatsApp, Facebook & Instagram) |
message_template.status_updated | Meta approved, rejected, paused, disabled or re-enabled a WhatsApp message template, or changed its quality score |
message_template.category_updated | Meta recategorized a WhatsApp message template — e.g. UTILITY → MARKETING |
placement.name_status_updated | Meta decided on a display name change request for a WhatsApp number |
placement.quality_updated | A WhatsApp number’s quality rating or messaging limit tier changed |
whatsapp.automatic_event | Meta detected a conversion (purchase, lead) in a Click-to-WhatsApp conversation |
You can subscribe to all events using * or select individual event types.
The WhatsApp-specific events — message_template.*, placement.name_status_updated, placement.quality_updated and whatsapp.automatic_event — are documented with their payloads on the WhatsApp platform page.
Event payload
Section titled “Event payload”Every webhook delivery uses a consistent envelope structure:
{ "id": "whevt_a1b2c3d4e5", "object": "event", "type": "platform_post.published", "created_at": "2025-01-15T10:30:00Z", "data": { "object": { // Event-specific resource (Post, Platform Post, Comment, Message, etc.) } }}| Field | Type | Description |
|---|---|---|
id | string | Delivery ID, prefixed with whevt_. Matches the X-Postproxy-Delivery header. |
object | string | Always "event". |
type | string | Event type (e.g. platform_post.published). Matches the X-Postproxy-Event header. |
created_at | string | ISO 8601 timestamp of when the event was generated. |
data.object | object | The event-specific resource — shape varies per event type (see Payloads by event). |
Headers
Section titled “Headers”| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Postproxy-Webhooks/1.0 |
X-Postproxy-Event | The event type (e.g. platform_post.published) |
X-Postproxy-Delivery | Unique delivery ID for this event |
X-Postproxy-Signature | HMAC signature for verification |
Payloads by event
Section titled “Payloads by event”post.processed
Section titled “post.processed”Uses the Post payload shape.
{ "id": "whevt_abc123", "object": "event", "type": "post.processed", "created_at": "2025-01-15T10:30:00Z", "data": { "object": { "id": "abc123xyz", "body": "Check out our latest update!", "status": "processed", "scheduled_at": null, "created_at": "2025-01-15T10:30:00Z", "platforms": [ { "id": "prof_xyz", "platform": "twitter", "name": "My Twitter" } ] } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Post id |
data.object.body | string | Post content |
data.object.status | string | Post status |
data.object.scheduled_at | string | null | ISO 8601 scheduled time |
data.object.created_at | string | ISO 8601 creation time |
data.object.platforms | array | Profiles attached to the post |
data.object.platforms[].id | string | Profile id |
data.object.platforms[].platform | string | Network name (e.g. twitter, linkedin) |
data.object.platforms[].name | string | Profile display name |
post.imported
Section titled “post.imported”Sent when Postproxy imports a post from a connected social platform (i.e. a post that was published outside Postproxy and discovered during a profile sync).
{ "id": "whevt_k9m2x7", "object": "event", "type": "post.imported", "created_at": "2025-01-15T10:30:00Z", "data": { "object": { "id": "q7d3k8", "body": "Check out our latest update!", "source": "imported", "posted_at": "2025-01-14T18:22:00Z", "created_at": "2025-01-15T10:30:00Z", "platform": "instagram", "profile": { "id": "m1x5b9", "name": "Acme Brand", "platform": "instagram" }, "platform_post_id": "17841405822304914", "public_id": "DEF456abc" } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Post ID |
data.object.body | string | Post content |
data.object.source | string | Always "imported" for imported posts |
data.object.posted_at | string | null | ISO 8601 timestamp the post was originally published on the platform |
data.object.created_at | string | ISO 8601 timestamp the post was imported into Postproxy |
data.object.platform | string | Network name (e.g. instagram, twitter) |
data.object.profile | object | Profile the post was imported from |
data.object.profile.id | string | Profile ID |
data.object.profile.name | string | Profile display name |
data.object.profile.platform | string | Network name |
data.object.platform_post_id | string | Platform’s internal post ID |
data.object.public_id | string | null | Platform’s public-facing post ID/shortcode (used in permalinks) |
platform_post.published, platform_post.failed, platform_post.failed_waiting_for_retry, platform_post.edited, platform_post.edit_failed
Section titled “platform_post.published, platform_post.failed, platform_post.failed_waiting_for_retry, platform_post.edited, platform_post.edit_failed”These events share the Platform Post payload shape. The status and error fields reflect the outcome. For platform_post.edited and platform_post.edit_failed, status stays published and error is set on failure; on X, platform_id is the new ID of the edited post. See Editing published posts.
{ "id": "whevt_def456", "object": "event", "type": "platform_post.published", "created_at": "2025-01-15T10:30:01Z", "data": { "object": { "id": "pp_abc123", "post_id": "abc123xyz", "platform": "twitter", "profile_id": "prof_xyz", "profile_name": "My Twitter", "status": "published", "error": null, "error_details": null, "platform_id": "1234567890" } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Platform post id |
data.object.post_id | string | Parent post id |
data.object.platform | string | Network name |
data.object.profile_id | string | Profile id |
data.object.profile_name | string | Profile display name |
data.object.status | string | published, failed, or failed_waiting_for_retry |
data.object.error | string | null | Error summary (null on success) |
data.object.error_details | object | null | Structured error details from the platform (null when no platform error info is available) |
data.object.error_details.platform_error_code | string | null | Error code returned by the platform API |
data.object.error_details.platform_error_subcode | string | null | Error subcode returned by the platform API |
data.object.error_details.platform_error_message | string | null | Error message returned by the platform API |
data.object.error_details.postproxy_note | string | null | Additional context from Postproxy about the error |
data.object.platform_id | string | null | External post ID on the platform (null until published) |
platform_post.insights
Section titled “platform_post.insights”Uses the Platform Post payload shape plus an insights field containing the latest normalized analytics snapshot, and a raw_stats field with the same metrics under their original platform key names.
{ "id": "whevt_ins789", "object": "event", "type": "platform_post.insights", "created_at": "2025-01-15T18:00:00Z", "data": { "object": { "id": "pp_abc123", "post_id": "abc123xyz", "platform": "twitter", "profile_id": "prof_xyz", "profile_name": "My Twitter", "status": "published", "error": null, "error_details": null, "platform_id": "1234567890", "insights": { "impressions": 1523, "likes": 42, "comments": 7, "shares": 3 }, "raw_stats": { "impression_count": 1523, "like_count": 42, "reply_count": 7 } } }}| Field | Type | Description |
|---|---|---|
data.object.insights | object | Normalized key-value stats from the platform (varies by network) |
data.object.raw_stats | object | null | The same metrics under their original platform key names (see Stats fields by platform). null for snapshots recorded before July 20, 2026 |
All other fields are identical to the Platform Post shape above.
profile.connected, profile.disconnected
Section titled “profile.connected, profile.disconnected”These two events share the Profile payload shape.
{ "id": "whevt_prof01", "object": "event", "type": "profile.disconnected", "created_at": "2025-01-15T12:00:00Z", "data": { "object": { "id": "prof_xyz", "name": "My Twitter", "platform": "twitter", "profile_group_id": "grp456xyz", "status": "disconnected", "uid": "twitter_456", "platform_id": "twitter_456", "username": "myhandle" } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Profile id |
data.object.name | string | Profile display name |
data.object.platform | string | Network name |
data.object.profile_group_id | string | Profile group ID |
data.object.status | string | active or disconnected |
data.object.uid | string | External ID on the platform |
data.object.platform_id | string | The account’s ID on the platform — same value as uid |
data.object.username | string | null | External username on the platform |
placement.orphaned, placement.restored
Section titled “placement.orphaned, placement.restored”Fired when a placement — a Facebook Page, LinkedIn organization, Pinterest board, Telegram channel, Google Business location — loses or regains a serving profile. placement.orphaned fires for each active placement a profile was serving when it disconnects (including via profile group deletion); the placement and all of its history remain readable, but stats collection, comment/DM ingestion and posting pause. placement.restored fires when the placement is served again — the same profile reconnected, or another connected profile with access to it took over (check data.object.profile_id to see which).
{ "id": "whevt_plc01", "object": "event", "type": "placement.orphaned", "created_at": "2026-08-26T12:00:00Z", "data": { "object": { "id": "967627696429808", "name": "My Page", "platform": "facebook", "profile_group_id": "grp456xyz", "profile_id": "prof_xyz", "profile_name": "John Doe", "metadata": {} } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Placement ID — the platform’s own ID, stable across disconnects and handovers; the same value List placements returns |
data.object.name | string | Placement display name |
data.object.platform | string | Network name |
data.object.profile_group_id | string | null | Profile group the placement is assigned to; null when unassigned |
data.object.profile_id | string | The serving profile — on orphaned the one that disconnected, on restored the one now serving it |
data.object.profile_name | string | That profile’s display name |
data.object.metadata | object | Extra per-placement data, same as List placements |
profile.placements_synced
Section titled “profile.placements_synced”Sent when a pull of a profile’s placements (Facebook Pages, LinkedIn organizations, Pinterest boards, Google Business locations) finishes. The first pull starts right after profile.connected and can take several minutes; later pulls run on the periodic refresh and when a sync is triggered manually. Only profiles on placement platforms emit it (telegram channels arrive via Telegram’s own webhook and never do).
{ "id": "whevt_p4l2c9", "object": "event", "type": "profile.placements_synced", "created_at": "2025-01-15T12:06:41Z", "data": { "object": { "id": "m1x5b9", "name": "My Facebook", "platform": "facebook", "profile_group_id": "grp456xyz", "status": "active", "uid": "123456789012345", "platform_id": "123456789012345", "username": null, "placements_sync_status": "synced", "placements": [ { "id": "967627696429808", "name": "Acme Store", "platform": "facebook", "profile_group_id": "grp456xyz", "profile_id": "m1x5b9", "profile_name": "My Facebook", "metadata": { "username": "acmestore" } }, { "id": "104558291234567", "name": "Acme Outlet", "platform": "facebook", "profile_group_id": "grp456xyz", "profile_id": "m1x5b9", "profile_name": "My Facebook", "metadata": {} } ] } }}| Field | Type | Description |
|---|---|---|
data.object.* | The Profile fields (id, name, platform, profile_group_id, status, uid, platform_id, username) | |
data.object.placements_sync_status | string | synced when the pull completed, failed when the platform rejected it (expired token, quota, upstream error). The next scheduled or manual sync retries |
data.object.placements | array | The full current list of placements this profile can post to — the same objects List placements returns, not a diff. Empty on failed unless an earlier pull already stored some. Can be long for multi-location Google Business accounts |
profile.stats
Section titled “profile.stats”Sent each time a new profile stats snapshot is recorded for a profile. Snapshots are captured roughly every 23 hours per profile. For networks with placements (facebook, linkedin, telegram) the event fires once per placement on each polling cycle.
{ "id": "whevt_stats01", "object": "event", "type": "profile.stats", "created_at": "2026-05-11T08:00:00Z", "data": { "object": { "profile_id": "prof_xyz", "platform": "linkedin", "placement_id": "108520199", "stats": { "followerCount": 4567, "shareCount": 10, "likeCount": 99, "allPageViews": 12728 }, "recorded_at": "2026-05-11T08:00:00Z" } }}| Field | Type | Description |
|---|---|---|
data.object.profile_id | string | Profile ID |
data.object.platform | string | Network name (e.g. linkedin, bluesky) |
data.object.placement_id | string | null | Platform-specific placement ID. null for networks without placements. |
data.object.stats | object | Platform-specific metrics. Shape varies by network — see Profiles · Stats fields by network. |
data.object.recorded_at | string | ISO 8601 timestamp when the snapshot was captured |
media.failed
Section titled “media.failed”Uses the Attachment payload shape.
{ "id": "whevt_med01", "object": "event", "type": "media.failed", "created_at": "2025-01-15T10:30:02Z", "data": { "object": { "id": "att_xyz", "post_id": "abc123xyz", "content_type": "image/jpeg", "status": "failed", "error_message": "Media file not found" } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Attachment id |
data.object.post_id | string | Parent post id |
data.object.content_type | string | MIME type of the attachment |
data.object.status | string | Always failed for this event |
data.object.error_message | string | Human-readable error description |
comment.created
Section titled “comment.created”Uses the Comment payload shape. comment.failed, comment.failed_waiting_for_retry and comment.delivery_unconfirmed use the same shape, with status set to the value the comment moved to. comment.edited and comment.edit_failed also use it; the comment stays published and external_edited_at marks the last confirmed edit.
{ "id": "whevt_h2c8f3", "object": "event", "type": "comment.created", "created_at": "2025-01-15T10:30:05Z", "data": { "object": { "id": "n5y1z7", "post_id": "q7d3k8", "platform_post_id": "j4r9v2", "platform": "instagram", "profile_id": "b8x2m4", "external_id": "ig_comment_456", "parent_external_id": null, "body": "Great post!", "status": "published", "error": null, "error_details": null, "author_external_id": "user_789", "author_name": "Jane Doe", "author_username": "janedoe", "author_avatar_url": "https://example.com/avatar.jpg", "like_count": 0, "reply_count": 0, "is_hidden": false, "permalink": "https://instagram.com/p/abc123/c/456", "platform_data": {}, "external_edited_at": null, "posted_at": null, "created_at": "2025-01-15T10:30:05Z" } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Comment ID |
data.object.post_id | string | Parent post ID |
data.object.platform_post_id | string | Platform post ID |
data.object.platform | string | Network name (e.g. instagram, facebook) |
data.object.profile_id | string | ID of the profile that owns the post |
data.object.external_id | string | null | Comment ID on the platform |
data.object.parent_external_id | string | null | Parent comment ID (null for top-level comments) |
data.object.body | string | Comment text |
data.object.status | string | published, failed, or failed_waiting_for_retry |
data.object.error | string | null | Error summary (null on success) |
data.object.error_details | object | null | Structured error details from the platform (null when no platform error info is available) |
data.object.error_details.platform_error_code | string | null | Error code returned by the platform API |
data.object.error_details.platform_error_subcode | string | null | Error subcode returned by the platform API |
data.object.error_details.platform_error_message | string | null | Error message returned by the platform API |
data.object.error_details.postproxy_note | string | null | Additional context from Postproxy about the error |
data.object.author_external_id | string | null | Author’s ID on the platform |
data.object.author_name | string | null | Author’s display name |
data.object.author_username | string | null | Author’s username |
data.object.author_avatar_url | string | null | Stable Postproxy storage URL of the author’s profile picture once mirrored; the platform URL until then |
data.object.like_count | integer | Number of likes |
data.object.reply_count | integer | Number of replies |
data.object.is_hidden | boolean | Whether the comment is hidden |
data.object.permalink | string | null | Direct link to the comment |
data.object.platform_data | object | null | Platform-specific metadata |
data.object.external_edited_at | string | null | ISO 8601 time of the last edit confirmed through Postproxy (null if never edited) |
data.object.posted_at | string | null | ISO 8601 time when posted on the platform |
data.object.created_at | string | ISO 8601 creation time |
profile_comment.created
Section titled “profile_comment.created”Fires when a new profile-scoped comment appears — either a newly synced incoming review (a Google Business review or a Facebook Page recommendation) or an outgoing reply that has just been published. See the Profile Comments API for the full resource.
Fires once per new review across all locations / Pages of the account. Webhook registration does not currently offer a placement filter — multi-location subscribers should filter on data.object.placement_id themselves.
{ "id": "whevt_pcm01", "object": "event", "type": "profile_comment.created", "created_at": "2026-05-13T06:00:01Z", "data": { "object": { "id": "abc123", "profile_id": "prof123abc", "platform": "google_business", "placement_id": "accounts/1234/locations/5678", "external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "parent_external_id": null, "body": "Great coffee, friendly staff!", "status": "synced", "error": null, "error_details": null, "author_username": "Jane D.", "author_avatar_url": "https://lh3.googleusercontent.com/...", "platform_data": { "star_rating": 5, "update_time": "2026-05-10T12:00:00Z" }, "posted_at": "2026-05-10T11:55:00Z", "created_at": "2026-05-13T06:00:01Z" } }}| Field | Type | Description |
|---|---|---|
data.object.id | string | Profile comment ID |
data.object.profile_id | string | Profile that owns the comment |
data.object.platform | string | Network name (currently google_business) |
data.object.placement_id | string | Location path (e.g. accounts/X/locations/Y) |
data.object.external_id | string | null | Platform’s native resource ID |
data.object.parent_external_id | string | null | Parent review external ID (null for top-level reviews) |
data.object.body | string | Comment/review text |
data.object.status | string | synced, published, pending, failed, failed_waiting_for_retry, or deleted |
data.object.error | string | null | Error summary (null on success) |
data.object.error_details | object | null | Structured error details from the platform (null when no platform error info is available) |
data.object.error_details.platform_error_code | string | null | Error code returned by the platform API |
data.object.error_details.platform_error_subcode | string | null | Error subcode returned by the platform API |
data.object.error_details.platform_error_message | string | null | Error message returned by the platform API |
data.object.error_details.postproxy_note | string | null | Additional context from Postproxy about the error |
data.object.author_username | string | null | Reviewer display name (null for your own replies) |
data.object.author_avatar_url | string | null | Reviewer avatar URL |
data.object.platform_data | object | null | Platform-specific metadata (e.g. star_rating) |
data.object.posted_at | string | null | ISO 8601 time when posted on the platform |
data.object.created_at | string | ISO 8601 creation time |
profile_comment.deleted
Section titled “profile_comment.deleted”Same payload shape as profile_comment.created, with status set to deleted. Fires when a Google Business review that Postproxy had previously synced is no longer returned by Google (removed by the reviewer or by Google moderation), or when a review’s owner reply was deleted outside Postproxy. Deleting a review cascades to its reply row, but only the review’s event is sent. Currently google_business only — Facebook recommendation removals already arrive through Meta’s ratings webhook.
Deleted rows drop out of GET /api/profiles/:id/comments but remain fetchable by id with status: "deleted".
message.received, message.sent, message.delivered, message.read, message.edited, message.deleted, message.failed_waiting_for_retry, message.failed
Section titled “message.received, message.sent, message.delivered, message.read, message.edited, message.deleted, message.failed_waiting_for_retry, message.failed”All eight message events share the Message payload shape — data.object is the full direct message object. The event type tells you what happened; inspect the message’s fields for the details.
{ "id": "whevt_msg01", "object": "event", "type": "message.received", "created_at": "2026-06-01T15:02:00Z", "data": { "object": { "id": "msg_111", "chat_id": "chat_xyz789", "external_id": "mid.abc123", "direction": "inbound", "body": "Hey, do you ship internationally?", "status": "received", "external_comment_id": null, "error_message": null, "platform_data": null, "external_posted_at": "2026-06-01T15:02:00.000Z", "external_delivered_at": null, "external_read_at": null, "external_edited_at": null, "reply_to_external_id": null, "reply_markup": null, "quick_replies": null, "buttons": null, "card": null, "tapped_action": null, "story_reply": null, "external_deleted_at": null, "reactions": [], "attachments": [], "is_unsupported": false, "created_at": "2026-06-01T15:02:01.000Z" } }}| Field | Type | Description |
|---|---|---|
data.object | object | The full message object, reflecting state after the event |
Per-platform firing rules:
message.delivered/message.readfire on Facebook & Instagram only — Telegram and Bluesky do not expose delivery/read receipts to bots.message.deletedfires on Instagram only — Messenger has no reliable deletion webhook for Pages, and Telegram does not notify bots of user deletions.message.editedfires for inbound edits on Facebook, Instagram, and Telegram (via Telegram’sedited_messageupdate) and for outbound Telegram edits made via the API.- A tapped quick reply, button, or ice breaker arrives as
message.receivedwithtapped_actionset to thepayloadyou sent — no need to readplatform_data.
reaction.received
Section titled “reaction.received”Fires when a reaction is added or removed on a message — for inbound (participant) reactions and outbound (business-initiated) reactions via the React API. Facebook & Instagram only.
{ "id": "whevt_rxn01", "object": "event", "type": "reaction.received", "created_at": "2026-06-01T15:02:00Z", "data": { "object": { "message": { "id": "msg_111", "chat_id": "chat_xyz789", "direction": "inbound", "body": "Hey, do you ship internationally?", "reactions": [ { "sender_external_id": "psid_123", "emoji": "❤️", "reaction": "love", "at": "2026-06-01T15:02:00.000Z" } ] }, "sender_external_id": "psid_123", "action": "react", "reaction": "love", "emoji": "❤️", "occurred_at": "2026-06-01T15:02:00Z" } }}| Field | Type | Description |
|---|---|---|
data.object.message | object | The full message object, reflecting the post-reaction state (the reactions array already has the reactor added for react, or removed for unreact) |
data.object.sender_external_id | string | Platform ID of whoever placed/removed the reaction (participant PSID/IGSID, or your own profile.external_id for business reactions) |
data.object.action | string | react or unreact |
data.object.reaction | string | null | Named reaction (e.g. love) |
data.object.emoji | string | null | Unicode emoji; may be absent on Messenger and on unreact events |
data.object.occurred_at | string | null | ISO 8601 time the reaction occurred |
referral.received
Section titled “referral.received”Fires when a participant enters a DM thread via an ig.me / m.me link carrying a ref parameter — whether the referral arrives standalone, embedded in the first inbound message, or inside an ice-breaker tap. Facebook & Instagram only; on Instagram, Meta only sends referrals for profiles with at least one ice breaker set. The referral is also stored on the chat under metadata.referral, and per Meta policy it opens the 24-hour messaging window.
{ "id": "whevt_ref01", "object": "event", "type": "referral.received", "created_at": "2026-06-10T12:00:00Z", "data": { "object": { "chat": { "id": "chat_xyz789", "profile_id": "prof_abc123", "platform": "instagram", "participant_external_id": "igsid_456", "metadata": { "referral": { "ref": "summer_promo", "source": "SHORTLINK", "type": "OPEN_THREAD", "received_at": "2026-06-10T12:00:00Z" }, "last_referral_at": "2026-06-10T12:00:00Z" } }, "ref": "summer_promo", "source": "SHORTLINK", "type": "OPEN_THREAD", "occurred_at": "2026-06-10T12:00:00Z" } }}| Field | Type | Description |
|---|---|---|
data.object.chat | object | The full chat object, with the referral already stored under metadata.referral |
data.object.ref | string | The ref parameter from the link |
data.object.source | string | Where the referral came from (e.g. SHORTLINK) |
data.object.type | string | Meta’s referral type (e.g. OPEN_THREAD) |
data.object.occurred_at | string | null | ISO 8601 time the referral occurred |
Signature verification
Section titled “Signature verification”Every webhook request includes an X-Postproxy-Signature header for verifying that the request came from Postproxy.
The signature format is:
t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— Unix timestamp when the signature was generatedv1— HMAC-SHA256 hex digest
The signed payload is {timestamp}.{body} where body is the raw JSON request body.
Verification examples
Section titled “Verification examples”Ruby
def verify_webhook(payload, signature_header, secret) parts = signature_header.split(",").map { |p| p.split("=", 2) }.to_h timestamp = parts["t"] expected = parts["v1"]
signed_payload = "#{timestamp}.#{payload}" computed = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
ActiveSupport::SecurityUtils.secure_compare(computed, expected)endNode.js
const crypto = require("crypto");
function verifyWebhook(payload, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(",").map((p) => p.split("=", 2)) ); const timestamp = parts.t; const expected = parts.v1;
const signedPayload = `${timestamp}.${payload}`; const computed = crypto .createHmac("sha256", secret) .update(signedPayload) .digest("hex");
return crypto.timingSafeEqual( Buffer.from(computed), Buffer.from(expected) );}Python
import hmacimport hashlib
def verify_webhook(payload: str, signature_header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in signature_header.split(",")) timestamp = parts["t"] expected = parts["v1"]
signed_payload = f"{timestamp}.{payload}" computed = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest()
return hmac.compare_digest(computed, expected)Retry policy
Section titled “Retry policy”If your endpoint returns a non-2xx status code or the request fails, Postproxy retries the delivery up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After all attempts are exhausted, the delivery is marked as failed. You can view delivery history via the API.
Your endpoint should return a 2xx response within 30 seconds. Connections that take longer than 10 seconds to establish will time out.
API endpoints
Section titled “API endpoints”| Method | Endpoint | Description |
|---|---|---|
GET | /api/webhooks | List all webhooks |
GET | /api/webhooks/:id | Get a single webhook |
POST | /api/webhooks | Create a webhook |
PATCH | /api/webhooks/:id | Update a webhook |
DELETE | /api/webhooks/:id | Delete a webhook |
GET | /api/webhooks/:id/deliveries | List delivery attempts |
List webhooks
Section titled “List webhooks”GET /api/webhooks
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/webhooks" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "data": [ { "id": "wh_abc123", "url": "https://example.com/webhooks", "events": ["post.processed", "platform_post.published"], "enabled": true, "description": "Production webhook", "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T10:00:00Z" } ]}Get webhook
Section titled “Get webhook”GET /api/webhooks/:id
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/webhooks/wh_abc123" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "id": "wh_abc123", "url": "https://example.com/webhooks", "events": ["post.processed", "platform_post.published"], "enabled": true, "description": "Production webhook", "secret": "whsec_a1b2c3d4e5f6...", "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T10:00:00Z"}The secret field is only included when fetching a single webhook or immediately after creation.
Create webhook
Section titled “Create webhook”POST /api/webhooks
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive webhook events |
events | array | Yes | Event types to subscribe to (use ["*"] for all) |
description | string | No | Description for the webhook |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/webhooks" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks", "events": ["post.processed", "platform_post.published"], "description": "Production webhook" }'Response (201 Created)
Section titled “Response (201 Created)”{ "id": "wh_abc123", "url": "https://example.com/webhooks", "events": ["post.processed", "platform_post.published"], "enabled": true, "description": "Production webhook", "secret": "whsec_a1b2c3d4e5f6...", "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T10:00:00Z"}Update webhook
Section titled “Update webhook”PATCH /api/webhooks/:id
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | No | New HTTPS URL |
events | array | No | New event types |
enabled | boolean | No | Enable or disable the webhook |
description | string | No | Updated description |
Example
Section titled “Example”curl -X PATCH "https://api.postproxy.dev/api/webhooks/wh_abc123" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }'Response
Section titled “Response”{ "id": "wh_abc123", "url": "https://example.com/webhooks", "events": ["post.processed", "platform_post.published"], "enabled": false, "description": "Production webhook", "secret": "whsec_a1b2c3d4e5f6...", "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T12:00:00Z"}Delete webhook
Section titled “Delete webhook”DELETE /api/webhooks/:id
Example
Section titled “Example”curl -X DELETE "https://api.postproxy.dev/api/webhooks/wh_abc123" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "deleted": true}List deliveries
Section titled “List deliveries”GET /api/webhooks/:id/deliveries
Retrieve delivery attempts for a specific webhook, useful for debugging.
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page | integer | No | 1 | Page number (1-based). Passing 0 returns page 1 |
per_page | integer | No | 20 | Number of deliveries per page |
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/webhooks/wh_abc123/deliveries" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "total": 15, "page": 1, "per_page": 20, "data": [ { "id": "abc123", "event_id": "whevt_abc123", "event_type": "post.processed", "response_status": 200, "attempt_number": 1, "success": true, "attempted_at": "2025-01-15T10:30:01Z", "created_at": "2025-01-15T10:30:01Z" } ]}