Webhooks
Create a webhook endpoint, receive your first event, and verify, deduplicate and retry deliveries the way Zernio sends them.
Zernio POSTs an event to your webhook endpoint when something happens: a post publishes, a DM arrives, an account disconnects, a number activates. Enable it with POST /v1/webhooks/settings. The secret Zernio signs deliveries with is the secret you send in that call; Zernio never generates one for you, and the same endpoints are editable in the webhooks dashboard.
First event
Call POST /v1/webhooks/settings with name, url, events and a secret. You can create up to 50 endpoints per user.
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: created } = await zernio.webhooks.createWebhookSettings({
body: {
name: 'Production',
url: 'https://example.com/webhooks/zernio',
secret: process.env.ZERNIO_WEBHOOK_SECRET,
events: ['post.published', 'post.failed', 'message.received']
}
});
const webhookId = created.webhook._id;Response (200):
{
"success": true,
"webhook": {
"_id": "507f1f77bcf86cd799439011",
"name": "Production",
"url": "https://example.com/webhooks/zernio",
"events": ["post.published", "post.failed", "message.received"],
"isActive": true,
"failureCount": 0
}
}webhook._id is the webhookId for the test call. Call POST /v1/webhooks/test to send a webhook.test event to the endpoint right away:
const { data: tested } = await zernio.webhooks.testWebhook({
body: { webhookId }
});
console.log(tested.message);Response (200):
{
"success": true,
"message": "Test webhook sent successfully"
}Your endpoint receives one POST with these headers and body:
POST /webhooks/zernio HTTP/1.1
Content-Type: application/json
User-Agent: Zernio-Webhooks/1.0
X-Zernio-Event: webhook.test
X-Zernio-Event-Id: 3f0c1c2e-6c4a-4d3e-9b1f-2a7d8e9f0a1b
X-Zernio-Signature: 5d41402abc4b2a76b9719d911017c592e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9{
"id": "3f0c1c2e-6c4a-4d3e-9b1f-2a7d8e9f0a1b",
"event": "webhook.test",
"message": "This is a test webhook from Zernio",
"timestamp": "2027-01-01T17:00:00Z"
}Return any 2xx within 5 seconds. webhook.test reaches the endpoint whatever its events list says, so it works before you subscribe to anything real. A 500 from the test call means your endpoint did not answer 2xx:
{
"success": false,
"message": "Test webhook failed"
}Check the endpoint's URL, then read the attempt in webhook logs or the dashboard.
Events
Every event is one event name in the events array. Each area page documents the payload of every event it covers.
Subscribe an endpoint only to the events it handles. Manage endpoints with List webhooks, Update webhook settings and Delete webhook settings.
Verification events
The two managed-OTP events have no area page: they belong to a verification, not to a connected account. verification.approved fires when the recipient submits the right code to Check a verification code, verification.failed when the attempts run out, with reason: "max_attempts_reached". Both carry verification.verificationId, verification.channel (sms) and verification.to. Send the code with Send a verification code.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
Example Requests
/verification.approvedRequest Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
Example Requests
/verification.failedHow it behaves
Delivery retries
Zernio delivers an event up to 7 times, counting the first attempt. A delivery succeeds when your endpoint returns a 2xx within 5 seconds. Any other outcome (a non-2xx status, a timeout, a connection error) schedules the next attempt on an exponential backoff capped at 24 hours, 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 failure Zernio moves the event to a dead-letter queue and stops retrying it. Every attempt is visible in webhook logs and the dashboard; attemptNumber says which try produced each entry, and Redeliver sends a dead-lettered event again.
To stay under the 5-second limit, persist the event and return 2xx, then process it on a background worker.
Zernio disables an endpoint only when it has had no successful delivery for 3 days and has either reached 20 consecutive terminal failures (events that exhausted every attempt) or been failing continuously for 3 days. One successful delivery inside that window keeps it enabled whatever the count. Zernio emails the owner; re-enable it with isActive: true on Update webhook settings. You can also pause or remove an endpoint yourself at any time.
At-least-once delivery
Zernio delivers every event at least once. The same event can arrive twice when a previous attempt's response was lost or your endpoint took longer than 5 seconds to answer, so your handler must be idempotent.
Every payload carries a stable event id, repeated as a header:
payload.id, the canonical event id (UUID).X-Zernio-Event-Id, the same value as a header.X-Late-Event-Id, the legacy alias, kept for backward compatibility.
Use it as your deduplication key: insert the id into a unique-indexed table or cache before processing, and skip the payload when the insert conflicts.
Signatures
Zernio signs every delivery when the endpoint has a secret. X-Zernio-Signature is the lowercase hex HMAC-SHA256 of the raw request body keyed by that secret; X-Late-Signature is the legacy alias with the same value. Read the raw body, compute the HMAC and compare:
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
// ...
};A mismatch means the request did not come from Zernio or the body changed in transit. Do not process it.
The test event
Zernio sends webhook.test to an endpoint when you call Test webhook or press test in the dashboard, whatever events the endpoint subscribes to. It is sent once, synchronously, and is never retried.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Webhook payload for test deliveries
Response Body
Example Requests
Webhook payload for test deliveries
/webhook.testRelated
- Create webhook settings: every field, including
customHeadersanddisabledResourceGroups. - Webhook logs: every attempt with its status code and response body.
- Post webhooks: the events behind a scheduled post.
- Inbox webhooks:
message.receivedand what it carries. - Multi-tenant: route events to the right customer by
account.profileId.