Sending

Webhooks

We POST to your server when something happens to a message you sent: it left, it arrived, it was read, it failed, or somebody replied. Optional, but the alternative is polling.

Setting one up

In the console, Developers, then Webhook. Give us a name, an https URL and the events that endpoint should receive. You get a signing secret, shown once.

Your endpoint should answer any 2xx within ten seconds. Anything else counts as a failure and we retry.

What you receive

json
POST /your/webhook
Content-Type: application/json
Sendrix-Signature: t=1787929823,v1=56868badbdea6b...
Sendrix-Event: message.delivered
Sendrix-Delivery: 0d8e6ac6-1d64-4d4f-9f4b-9c2a0e5b3f11

{
  "id": "0d8e6ac6-1d64-4d4f-9f4b-9c2a0e5b3f11",
  "type": "message.delivered",
  "created_at": "2026-08-28T15:10:22.985Z",
  "data": {
    "message_id": "msg_c16cead6-8385-481f-9e2e-e146d51f2738",
    "to": "919833663235",
    "status": "delivered",
    "wa_message_id": "wamid.HBgMOTE5ODMzNjYzMjM1FQIAERgSN...",
    "occurred_at": "2026-08-28T15:10:21.000Z",
    "error": null
  }
}

Events

EventFires when
message.sentMeta accepted the message. For a one-time code this is the first useful signal and arrives well before delivery.
message.deliveredIt reached the handset.
message.readThe recipient opened it. Only if they have read receipts on.
message.failedIt will not arrive. data.error carries Meta’s code.
message.receivedSomebody replied to you.

Verify the signature

Anyone who learns your URL can post to it. The signature is what proves a request came from us, so check it before you trust the body.

Sendrix-Signature carries a timestamp and a hash: t=1787929823,v1=56868b.... The hash is HMAC-SHA256 over the literal string {t}.{raw body}, keyed with your signing secret.

node
import crypto from "node:crypto";

// The RAW body, before any JSON parsing. Re-serialising changes the bytes
// and the signature will never match.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);

  // Reject anything older than five minutes. The timestamp is inside the
  // signature, so this is what stops a captured request being replayed.
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  // Constant time, so a wrong signature cannot be found one byte at a time.
  const a = Buffer.from(parts.v1 ?? "", "utf8");
  const b = Buffer.from(expected, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Retries

A failed delivery is retried seven times over about nine hours: immediately, then after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Short at first because most failures are a deploy that lasted seconds.

Every attempt is recorded with what your server answered and how long it took, visible under Developers, then Webhook. You can retry any delivery by hand from there, which is what you do the moment after fixing your endpoint.

We switch it off eventually

After twenty consecutive failures the endpoint is disabled and the reason recorded. Something failing that consistently is gone rather than busy. Fix it, then turn it back on in the console and we resume.

Reading your endpoints back

GET /v1/webhook lists them, with the webhooks:read scope. Configuring them stays in the console: pointing a webhook somewhere new is a trust decision a person should make while signed in, not something an integration can do to itself with a key it already holds.

json
{
  "object": "list",
  "data": [
    {
      "id": "whk_9f2a4c1b-...",
      "object": "webhook",
      "name": "Order service",
      "url": "https://acme.io/hooks/sendrix",
      "secret_hint": "whsec_9f2a...4c1b",
      "events": ["message.delivered", "message.failed"],
      "enabled": true,
      "disabled_reason": null,
      "last_success_at": "2026-08-28T15:10:22.985Z"
    }
  ],
  "total": 1
}

Writing a good receiver

  • Answer fast, work later. Acknowledge with a 200 and do your processing on a queue. Ten seconds is the limit and a slow receiver becomes a failing one.
  • Deduplicate on id. Delivery is at-least-once on purpose: we would rather send a receipt twice than lose one. The same event id may arrive more than once.
  • Do not assume order. Retries mean delivered can arrive after read. Use data.occurred_at if sequence matters.
  • Ignore what you do not know. New event types and new fields will appear. See Versioning.