Webhooks
Receive inbound messages, delivery statuses and account events at your own endpoint.
Msgera POSTs a signed JSON payload to your endpoint when something happens on a connected device: an inbound message, a delivery status update, an edit, delete or reaction, or a device status change. You configure two independent URLs per device - one for 1:1 chats (personal) and one for group chats (group). Configure them via the dashboard or with PUT /v1/developer/webhooks.
Configure your URLs
curl -X PUT https://api.msgera.io/v1/developer/webhooks \
-H "X-API-Key: $MSGERA_KEY" \
-H "Content-Type: application/json" \
-d '{
"deviceUid": "dev_4f8c2a1e",
"personalWebhookUrl": "https://app.example.com/wa/incoming",
"groupWebhookUrl": "https://app.example.com/wa/groups",
"isActive": true
}'| Field | Type | Description |
|---|---|---|
deviceUidrequired | string | UID of the device to configure. |
personalWebhookUrl | string | HTTPS URL that receives 1:1 chat events, account status events and campaign variation events. |
groupWebhookUrl | string | HTTPS URL that receives group chat events. |
isActive | boolean | Toggle delivery on/off without losing the URLs. |
URLs must be public HTTPS endpoints; private, loopback and link-local addresses are rejected.
Choose events
Each webhook subscribes to the events it wants. Pick them in the dashboard under Developer → Webhooks. Webhooks created without a selection, including every webhook created with PUT /v1/developer/webhooks and every webhook that existed before subscriptions were introduced, receive message.inbound only. Updating a webhook's URLs with PUT /v1/developer/webhooks keeps its subscriptions and filters.
| Field | Type | Description |
|---|---|---|
message.inbound | event | Messages your customers send. |
message.status | event | SENT, DELIVERED, READ and FAILED updates for messages you sent. |
message.edited | event | A message was edited. |
message.deleted | event | A message was deleted for everyone. |
message.reaction | event | An emoji reaction was added, changed or removed. |
account.status | event | A device connected, disconnected, needs a QR scan, or was restricted. |
Payloads for every event are on Webhook events. Campaign variation events are always sent to your personal URL and do not need a subscription.
Filter message events
Filters narrow which message events a webhook receives. A filter is a list of conditions and a delivery is sent only when all conditions match. Within one condition, a value list matches when any of its values matches. Without conditions, every subscribed event is delivered. Filters apply to message.inbound, message.status, message.edited, message.deleted and message.reaction; they do not apply to account.status or campaign variation events. Build them in the dashboard under Developer → Webhooks.
[
{ "field": "isGroup", "op": "is", "values": ["false"] },
{ "field": "from", "op": "isNot", "values": ["15551234567", "15557654321"] },
{ "field": "text", "op": "contains", "values": ["order", "invoice"] }
]| Field | Type | Description |
|---|---|---|
fieldrequired | string | from | chatId | text | type | isGroup. |
oprequired | string | is | isNot | contains. |
valuesrequired | string[] | 1 to 100 values, each up to 500 characters. |
| Field | Type | Description |
|---|---|---|
from | string | The other party of the chat: the sender of an inbound message, the recipient of a status update, or the person who edited, deleted or reacted. |
chatId | string | The group id in group chats, otherwise the same as from. |
text | string | Message text; the new text for edits and the emoji for reactions. |
type | string | text | image | video | audio | document | sticker. |
isGroup | string | "true" or "false". |
| Field | Type | Description |
|---|---|---|
is | op | The field equals one of the values. |
isNot | op | The field equals none of the values. |
contains | op | The field contains at least one of the values. |
Matching rules
+1 (555) 123-4567 matches 15551234567. A webhook can have up to 20 conditions with up to 100 values each.Payload
Msgera POSTs the event payload as the raw request body with Content-Type: application/json. The full field reference lives on Webhook events.
Request headers
| Field | Type | Description |
|---|---|---|
X-Msgera-Signature | string | v1=<hex>, the HMAC-SHA256 of "<timestamp>.<raw body>" using your signing secret. |
X-Msgera-Timestamp | string | Unix time in seconds when the delivery attempt was signed. New on every attempt. |
X-Msgera-Event | string | Event type, for example message.inbound or message.status. |
X-Msgera-Delivery-Id | string | Id of this delivery (dlv_...). The same for all retries of a delivery; a manual redelivery gets a new one. |
X-Msgera-Idempotency-Key | string | Stable id of the underlying event, identical across retries and redeliveries. Dedupe on this value. |
X-Msgera-Retry-Count | string | Number of earlier attempts for this delivery: 0 on the first attempt. |
Verify the signature
Every request is signed so you can confirm it came from Msgera. Each webhook has its own signing secret (starts with whsec_). Reveal or rotate it in the dashboard under Developer → Webhooks. The signature is sent in X-Msgera-Signature and covers the X-Msgera-Timestamp value and the raw body.
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyMsgeraSignature(rawBody, headers, secret) {
const timestamp = headers["x-msgera-timestamp"];
const signature = headers["x-msgera-signature"] ?? "";
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const expected =
"v1=" +
crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Use the raw body
Handle deliveries
Deliveries are at-least-once: a retry or redelivery can reach you after you already processed the event. Store each X-Msgera-Idempotency-Key you handle and skip keys you have seen. Respond with a 2xx quickly and do slow work in the background.
import express from "express";
import { verifyMsgeraSignature } from "./verify-msgera-signature.js";
const app = express();
const seen = new Set(); // use a database or Redis in production
app.post("/wa/incoming", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verifyMsgeraSignature(rawBody, req.headers, process.env.MSGERA_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const key = req.headers["x-msgera-idempotency-key"];
if (seen.has(key)) return res.sendStatus(200);
seen.add(key);
const payload = JSON.parse(rawBody);
switch (req.headers["x-msgera-event"]) {
case "message.inbound":
// flat payload: payload.from, payload.text, payload.mediaId ...
break;
case "message.status":
// envelope: payload.data.status, payload.data.providerMessageId ...
break;
}
res.sendStatus(200);
});Retries
Msgera expects a 2xx response within 10 seconds. At most one redirect is followed. Non-2xx responses and timeouts are retried, for up to 8 attempts in total. The delay starts at 5 seconds and doubles after each failed attempt (5s, 10s, 20s, 40s, 80s, 160s, 320s), and is never longer than 1 hour.
- The webhook configuration is read again before every attempt. Disabling the webhook, unsubscribing from the event, or removing the URL stops pending retries; a changed URL is used from the next attempt.
- Each attempt is signed with a fresh
X-Msgera-Timestamp, whileX-Msgera-Delivery-IdandX-Msgera-Idempotency-Keystay the same. - When your endpoint's last attempt failed, Msgera sends it at most two deliveries at a time until it succeeds again. Other deliveries wait their turn without using up attempts, so a failing endpoint receives a gentle stream instead of a burst when it recovers.
Failed deliveries
A delivery that fails all 8 attempts is kept for 90 days with its original payload, the number of attempts, and the last status code and error. Find them in the dashboard under Developer → Failed Deliveries. Click Redeliverto queue the original payload again to the webhook's current URL. A redelivery uses the same X-Msgera-Idempotency-Key as the original, so endpoints that dedupe on it stay safe, and gets a new X-Msgera-Delivery-Id and a fresh set of attempts. The webhook must be active and still have a URL for the chat type to redeliver.
Inspect delivery history
Every attempt, successful or failed, is recorded with its event, delivery id, idempotency key and response status code. Inspect them via GET /v1/developer/webhooks/history or in the dashboard under Developer → Webhooks.
curl "https://api.msgera.io/v1/developer/webhooks/history?deviceUid=dev_4f8c2a1e&status=FAILED" \
-H "X-API-Key: $MSGERA_KEY"| Field | Type | Description |
|---|---|---|
deviceUid | string | Filter by device. |
status | string | SUCCESS or FAILED. |
page | integer | 1-based page number. |
limit | integer | Page size. |