ChatmaidDevelopers

Webhooks

Consume webhook events, verify signatures, and handle retries.

Webhook Overview

Webhooks are optional. You can poll message status using GET /v1/messages/:messageId, or receive push updates on your webhook endpoint.

Webhook destination and subscribed events are configured in the Chatmaid dashboard.

Use a single API domain; environment is inferred from key prefix:

  • https://developers-api.chatmaid.net + sk_test_* for sandbox behavior
  • https://developers-api.chatmaid.net + sk_live_* for production behavior

Integration host: https://developers-api.chatmaid.net.

Events and Signatures

Supported events include:

  • message.sent — a message you sent was accepted by WhatsApp
  • message.failed — a message you sent could not be delivered
  • message.received — one of your connected phone numbers received an inbound message
  • message.delivered — a message you sent was delivered to the recipient's device
  • message.read — a message you sent was read by the recipient
  • phone.connected — a phone number connected to WhatsApp
  • phone.disconnected — a phone number disconnected from WhatsApp

message.delivered and message.read are driven by WhatsApp receipts: read may never fire if the recipient has read receipts disabled, and a message can jump from sent straight to read.

Each webhook includes these headers:

  • Content-Type: application/json
  • X-Chatmaid-Event
  • X-Chatmaid-Signature
import crypto from "crypto";

function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
  // signature format: t=1700000000,v1=<hex>
  const pairs = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const idx = part.indexOf("=");
      return [part.slice(0, idx), part.slice(idx + 1)];
    })
  );
  const timestamp = pairs.t;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(timestamp + "." + rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(pairs.v1), Buffer.from(expected));
}
import hmac
import hashlib


def verify_signature(raw_body: str, signature_header: str, secret: str) -> bool:
    # signature format: t=1700000000,v1=<hex>
    parts = dict(part.split("=", 1) for part in signature_header.split(","))
    payload = (parts["t"] + "." + raw_body).encode("utf-8")
    expected = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts.get("v1", ""), expected)
# Webhook requests are sent by Chatmaid to your server.
# Use cURL locally only to simulate payload delivery to your endpoint:
curl -X POST https://your-receiver.example/webhooks/chatmaid \
  -H "Content-Type: application/json" \
  -H "X-Chatmaid-Event: message.sent" \
  -H "X-Chatmaid-Signature: t=1700000000,v1=<signature>" \
  -d '{"event":"message.sent","timestamp":"2026-02-06T14:20:00.000Z","data":{"messageId":"msg_abc123"}}'

Payloads

Message lifecycle events (message.sent, message.delivered, message.read, message.failed) share the same data shape:

{
  "event": "message.sent",
  "timestamp": "2026-02-06T14:20:00.000Z",
  "data": {
    "messageId": "msg_abc123def456",
    "from": "+15551234567",
    "to": "+15557654321",
    "status": "sent",
    "sentAt": "2026-02-06T14:19:12.000Z",
    "deliveredAt": null,
    "readAt": null,
    "failedAt": null,
    "errorCode": null,
    "errorMessage": null
  }
}

message.received carries the inbound message:

{
  "event": "message.received",
  "timestamp": "2026-02-06T14:21:09.000Z",
  "data": {
    "messageId": "inmsg_abc123def456",
    "from": "+15557654321",
    "to": "+15551234567",
    "content": "Thanks, got it!",
    "type": "text",
    "isGroup": false,
    "groupId": null,
    "receivedAt": "2026-02-06T14:21:08.000Z"
  }
}

For message.received, from is the external sender and to is your connected phone number. type is one of text, image, video, audio, document, sticker, location, contact, other; for media messages, content carries the caption when present.

Retries and Debugging

Failed webhook deliveries are retried up to 3 attempts with exponential backoff: 1 minute, 5 minutes, then 15 minutes after the initial attempt. Your endpoint should return HTTP 2xx quickly and process events asynchronously.

Idempotent Receiver Required

Your webhook handler must be idempotent. Retries can deliver the same event more than once.