Webhooks
How Zernio webhook deliveries work and the payload sent for each event.
How to think about webhooks
- Subscribe only to the events you actually handle.
- Treat each delivery as an event notification, not a full source of truth sync.
- Use the webhook event ID as your deduplication key.
- Verify the
X-Zernio-Signatureheader when you configure a webhook secret. - Expect fast acknowledgement from your endpoint and move heavier processing to async jobs.
Delivery flow
- Create a webhook endpoint with Create webhook settings.
- Choose the events you want to subscribe to.
- Receive a
POSTrequest from Zernio whenever one of those events occurs. - Return a
2xxresponse after you have accepted the payload. - Use Test webhook and Webhook logs to validate your integration.
Delivery retries
A delivery is considered successful when your endpoint returns a 2xx response within 5 seconds. Any other outcome (non-2xx status, request timeout, connection error) triggers a retry on an exponential backoff schedule capped at 24 hours.
Up to 7 attempts are made per event. The full schedule, measured from the moment the previous attempt finished:
| Attempt | Delay before this attempt | Cumulative time since the first attempt |
|---|---|---|
| 1 | immediate | 0 |
| 2 | 10s | ~10s |
| 3 | 1m 40s | ~1m 50s |
| 4 | 16m 40s | ~18m 30s |
| 5 | 2h 46m 40s | ~3h 5m |
| 6 | 24h (capped) | ~27h 5m |
| 7 | 24h (capped) | ~51h 5m |
After the 7th attempt fails the event is moved to a dead-letter queue and is no longer retried automatically. Failures are visible via Webhook logs (attemptNumber records which try produced each log entry). Webhooks are never auto-disabled based on failure count, you can pause or remove them from your webhook settings.
Keep your handler fast. Acknowledge the request as soon as you have persisted the event, then process it on a background worker. Long-running handlers risk hitting the 5-second timeout and triggering an unnecessary retry.
Idempotency
Webhook deliveries use at-least-once semantics: the same event may arrive more than once if a previous attempt's response was lost or your endpoint took too long to acknowledge. Your handler must therefore be idempotent.
Every payload carries a stable event identifier that is also exposed as a header:
payload.id, the canonical event ID (UUID).X-Zernio-Event-Id, the same value, repeated as a header for convenience.X-Late-Event-Id, legacy alias of the above, kept for backward compatibility.
Use this identifier as your deduplication key. A typical pattern is to insert the event ID into a unique-indexed table or cache before processing the payload, and skip processing when the insert conflicts.
Signature verification
If the webhook has a secret configured, every delivery includes an X-Zernio-Signature header. The signature is the lowercase hex HMAC-SHA256 of the raw request body keyed by your webhook secret.
X-Zernio-Signature, the signature.X-Late-Signature, legacy alias of the above, kept for backward compatibility.
Read the raw body, compute the HMAC, and compare it to the header value:
import crypto from "crypto";
export const POST = async (req: Request) => {
const webhookSignature = req.headers.get("X-Zernio-Signature");
if (!webhookSignature) {
return new Response("No signature provided.", { status: 401 });
}
const secret = process.env.ZERNIO_WEBHOOK_SECRET;
if (!secret) {
return new Response("No secret provided.", { status: 401 });
}
const rawBody = await req.text();
const computedSignature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
if (webhookSignature !== computedSignature) {
return new Response("Invalid signature", { status: 400 });
}
const payload = JSON.parse(rawBody);
// Handle the webhook event
// ...
};Reject unsigned or mismatched requests. A failed signature check means the request did not originate from Zernio (or the body was tampered with in transit). Do not process the payload.
Available events
| Event | Description |
|---|---|
post.published | Fired when a post is successfully published. |
post.failed | Fired when a post fails to publish on all target platforms. |
post.partial | Fired when a post publishes on some platforms and fails on others. |
post.cancelled | Fired when a post publishing job is cancelled. |
post.scheduled | Fired when a post is scheduled for future publishing. |
post.recycled | Fired when a post is recycled for republishing. |
account.connected | Fired when a social account is successfully connected. |
account.disconnected | Fired when a connected social account becomes disconnected. |
account.ads.initial_sync_completed | Fired once per ads-enabled account when the initial 90-day backfill completes. |
message.received | Fired when a new inbox message is received. |
message.sent | Fired when an outgoing message is sent from the inbox. |
message.edited | Fired when a sender edits a previously-sent message. |
message.deleted | Fired when a sender deletes (unsends) a message. |
message.delivered | Fired when an outgoing message is delivered to the recipient. |
message.read | Fired when an outgoing message is read by the recipient. |
message.failed | Fired when an outgoing message fails to deliver (WhatsApp only). |
comment.received | Fired when a new comment is received on a tracked post. |
review.new | Fired when a new review is posted on a connected account. |
review.updated | Fired when a review is edited or a reply is added. |
webhook.test | Fired when sending a test webhook to verify the endpoint configuration. |
post.published
Fired when a post is successfully published. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"post.scheduled" | "post.published" | "post.failed" | "post.partial" | "post.cancelled" | "post.recycled"date-timeResponse Body
post.failed
Fired when a post fails to publish on all target platforms. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"post.scheduled" | "post.published" | "post.failed" | "post.partial" | "post.cancelled" | "post.recycled"date-timeResponse Body
post.partial
Fired when a post publishes on some platforms and fails on others. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"post.scheduled" | "post.published" | "post.failed" | "post.partial" | "post.cancelled" | "post.recycled"date-timeResponse Body
post.cancelled
Fired when a post publishing job is cancelled. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"post.scheduled" | "post.published" | "post.failed" | "post.partial" | "post.cancelled" | "post.recycled"date-timeResponse Body
post.scheduled
Fired when a post is scheduled for future publishing. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"post.scheduled" | "post.published" | "post.failed" | "post.partial" | "post.cancelled" | "post.recycled"date-timeResponse Body
post.recycled
Fired when a post is recycled for republishing. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"post.scheduled" | "post.published" | "post.failed" | "post.partial" | "post.cancelled" | "post.recycled"date-timeResponse Body
account.connected
Fired when a social account is successfully connected. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"account.connected"date-timeResponse Body
account.disconnected
Fired when a connected social account becomes disconnected. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"account.disconnected"date-timeResponse Body
account.ads.initial_sync_completed
Fired once per ads-enabled account when the initial sync completes. The initial sync runs after an ads-capable account is connected and performs ad-account discovery plus a 90-day historical ad backfill. The payload includes a sync summary reporting whether the backfill succeeded fully or partially and how many ads were synced vs. failed.
When scoping was applied at connect time (see Scoping sync to specific ad accounts), account.platformAdAccountId echoes the chosen ad account back (when scope is exactly one) and account.platformAdAccountIds lists every act_* actually synced.
On failure (sync.status == "failure"), the payload also carries optional fields to help branch your UX without parsing prose:
sync.error: raw platform error message (truncated to ~2KB).sync.errorCode/sync.errorSubcode: platform-native error codes when parseable (e.g. Meta190,10).sync.errorCategory: a stable enum, one oftoken_invalid,permission_denied,no_ad_accounts,rate_limited,discovery_failed,unknown. New values may be added; existing ones are stable.
Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"account.ads.initial_sync_completed"Summary of the initial ads sync backfill results.
date-timeResponse Body
message.received
Fired when a new inbox message is received. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"message.received"Interactive message metadata (present when message is a quick reply tap, postback button tap, or inline keyboard callback)
date-timeResponse Body
message.sent
Fired when an outgoing message is sent from the inbox. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"message.sent"date-timeResponse Body
message.edited
Fired when the sender edits a previously-sent message. Supported on Instagram, Facebook Messenger, and Telegram. The payload carries the full editHistory (oldest prior version first) so you can reconstruct every version of the message. message.text is the latest version. Subscribe with Create webhook settings or Update webhook settings.
"message.edited"The message object included in inbox webhook payloads.
Prior versions of the message, oldest first.
Total number of edits applied to this message.
When the most recent edit happened.
date-timeThe conversation context included in inbox webhook payloads.
The account context included in inbox webhook payloads.
date-timeResponse Body
message.deleted
Fired when the sender deletes (unsends) a message. Supported on Instagram (incoming unsend) and WhatsApp (when the business deletes an outgoing message via the Cloud API). The payload retains the pre-delete text and attachments so API consumers can access the original content, useful for moderation, compliance, or archival. The Zernio dashboard UI does not show this content. Subscribe with Create webhook settings or Update webhook settings.
"message.deleted"The message object included in inbox webhook payloads.
date-timeThe conversation context included in inbox webhook payloads.
The account context included in inbox webhook payloads.
date-timeResponse Body
message.delivered
Fired when an outgoing message is delivered to the recipient. Supported on WhatsApp and Facebook Messenger. Instagram doesn't emit a separate delivery event (only message.read). Subscribe with Create webhook settings or Update webhook settings.
"message.delivered" | "message.read" | "message.failed"The message object included in inbox webhook payloads.
When the platform reported this status.
date-timePopulated only on message.failed.
The conversation context included in inbox webhook payloads.
The account context included in inbox webhook payloads.
date-timeResponse Body
message.read
Fired when an outgoing message is read by the recipient. Supported on WhatsApp, Facebook Messenger, and Instagram. Subscribe with Create webhook settings or Update webhook settings.
"message.delivered" | "message.read" | "message.failed"The message object included in inbox webhook payloads.
When the platform reported this status.
date-timePopulated only on message.failed.
The conversation context included in inbox webhook payloads.
The account context included in inbox webhook payloads.
date-timeResponse Body
message.failed
Fired when an outgoing message fails to deliver. Currently only emitted for WhatsApp (Messenger, Instagram, and Telegram don't expose per-message failure via webhook). The payload error object includes code, title, and message from the platform, useful for categorising failures (e.g. 131026 for "recipient phone not reachable"). Subscribe with Create webhook settings or Update webhook settings.
"message.delivered" | "message.read" | "message.failed"The message object included in inbox webhook payloads.
When the platform reported this status.
date-timePopulated only on message.failed.
The conversation context included in inbox webhook payloads.
The account context included in inbox webhook payloads.
date-timeResponse Body
comment.received
Fired when a new comment is received on a tracked post. Subscribe with Create webhook settings or Update webhook settings.
The payload includes an optional comment.ad object when the comment was made on paid content. For Instagram this carries ad.id and ad.title directly from the Meta webhook. For Facebook it carries ad.promotionStatus ("active" for boosted organic posts, "ineligible" for dark post creatives). The field is absent for comments on organic posts that are not currently promoted, so clients can filter ad-driven comments with a simple if (comment.ad) { ... } check.
Stable webhook event ID
"comment.received"date-timeResponse Body
review.new
Fired when a new review is posted on a connected account. Currently supported for Google Business Profile (real-time via Pub/Sub). Available on the Usage plan (or AppSumo with the Inbox add-on). Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"review.new"Review data shared by review.new and review.updated payloads.
date-timeResponse Body
review.updated
Fired when a review changes: the reviewer edits their text or rating, or a reply is added (via the API or directly through the Google Business dashboard). Available on the Usage plan (or AppSumo with the Inbox add-on). The payload has the same shape as review.new; when a reply is present, review.hasReply is true and review.reply is populated. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"review.updated"Review data shared by review.new and review.updated payloads.
date-timeResponse Body
webhook.test
Fired when sending a test webhook to verify the endpoint configuration. Subscribe with Create webhook settings or Update webhook settings.
Stable webhook event ID
"webhook.test"Human-readable test message
date-time