sgeraDocs
Core concepts

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
  }'
FieldTypeDescription
deviceUidrequiredstringUID of the device to configure.
personalWebhookUrlstringHTTPS URL that receives 1:1 chat events, account status events and campaign variation events.
groupWebhookUrlstringHTTPS URL that receives group chat events.
isActivebooleanToggle 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.

FieldTypeDescription
message.inboundeventMessages your customers send.
message.statuseventSENT, DELIVERED, READ and FAILED updates for messages you sent.
message.editedeventA message was edited.
message.deletedeventA message was deleted for everyone.
message.reactioneventAn emoji reaction was added, changed or removed.
account.statuseventA 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"] }
]
FieldTypeDescription
fieldrequiredstringfrom | chatId | text | type | isGroup.
oprequiredstringis | isNot | contains.
valuesrequiredstring[]1 to 100 values, each up to 500 characters.
Fields
FieldTypeDescription
fromstringThe 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.
chatIdstringThe group id in group chats, otherwise the same as from.
textstringMessage text; the new text for edits and the emoji for reactions.
typestringtext | image | video | audio | document | sticker.
isGroupstring"true" or "false".
Operators
FieldTypeDescription
isopThe field equals one of the values.
isNotopThe field equals none of the values.
containsopThe field contains at least one of the values.

Matching rules

Comparisons ignore case and surrounding whitespace. Phone numbers ignore formatting, so +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

FieldTypeDescription
X-Msgera-Signaturestringv1=<hex>, the HMAC-SHA256 of "<timestamp>.<raw body>" using your signing secret.
X-Msgera-TimestampstringUnix time in seconds when the delivery attempt was signed. New on every attempt.
X-Msgera-EventstringEvent type, for example message.inbound or message.status.
X-Msgera-Delivery-IdstringId of this delivery (dlv_...). The same for all retries of a delivery; a manual redelivery gets a new one.
X-Msgera-Idempotency-KeystringStable id of the underlying event, identical across retries and redeliveries. Dedupe on this value.
X-Msgera-Retry-CountstringNumber 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

Compute the HMAC over the exact bytes you received, before any JSON parsing. Reject requests older than 5 minutes to block replays, and always compare signatures in constant time.

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, while X-Msgera-Delivery-Id and X-Msgera-Idempotency-Key stay 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"
FieldTypeDescription
deviceUidstringFilter by device.
statusstringSUCCESS or FAILED.
pageinteger1-based page number.
limitintegerPage size.