Skip to content

Webhooks

Subscriptions are created through the API or the app. Every delivery is signed with the secret returned when the subscription was created.

Subscribing

POST a public https URL and the event names to /webhooks. The response includes signing_secret once; store it. Available events: message.received, message.status, template.status, broadcast.completed, contact.opted_out, conversation.status_changed, lead.created. A subscription that fails repeatedly is switched to disabled; enable it again with a PUT.

What we send

A POST with a JSON body { id, event, createdAt, data } and these headers:

  • x-edwyz-event: the event name.
  • x-edwyz-delivery: delivery id, the same across retries.
  • x-edwyz-timestamp: seconds since the epoch when this attempt was signed.
  • x-edwyz-signature: sha256= followed by the hex HMAC-SHA256 of the timestamp, a dot, and the exact raw body, keyed with your secret.

Answer with any 2xx status within 10 seconds. Redirects are not followed. Failed deliveries are retried, so deduplicate on the id in the body.

Events

The dataobject of each event is shown below. Fields that can be missing are null, never absent. A person who messages you through a WhatsApp username has no phone number: use the contact's bsuid to recognise them.

message.received

A customer sends a message. After an incoming WhatsApp message is stored. Reactions and status updates do not use this event.

  • conversationId (string) Conversation the message belongs to.
  • channel (string or null) Meta phone_number_id the message came in on.
  • assignedTo (string or null) User id of the agent the conversation is assigned to, if any.
  • contact.id (string) Id of the contact in the API.
  • contact.phone (string or null) Phone number with country code. Null for people who message through a WhatsApp username.
  • contact.bsuid (string or null) WhatsApp business-scoped user id (BSUID). Always present once the person has messaged you through WhatsApp; use it as the stable key when phone is null.
  • contact.waId (string or null) WhatsApp id (the phone number as digits), when WhatsApp shared it.
  • contact.username (string or null) WhatsApp username, when known.
  • contact.name (string or null) Display name.
  • message.id (string) WhatsApp message id (wamid).
  • message.type (string) text, image, audio, video, document, sticker, location, contacts, button, interactive or other.
  • message.text (string or null) Message text or caption, when there is one.
  • message.mediaId (string or null) WhatsApp media id for media messages.
  • message.mimeType (string or null) Media type for media messages.
  • message.timestamp (string) When WhatsApp says the message was sent, ISO 8601.
  • referral (object or null) Present when the chat started from a click-to-WhatsApp ad.
  • referral.sourceType (string or null) ad or post.
  • referral.sourceId (string or null) Id of the ad or post.
  • referral.sourceUrl (string or null) Link of the ad or post.
  • referral.headline (string or null) Ad headline.
  • referral.ctwaClid (string or null) Click id, for conversion reporting.
{
  "conversationId": "ckconv1",
  "channel": "109876543210987",
  "assignedTo": null,
  "contact": {
    "id": "ckcontact1",
    "phone": "+919876543210",
    "bsuid": "IN.13491208655302741918",
    "waId": "919876543210",
    "username": null,
    "name": "Asha"
  },
  "message": {
    "id": "wamid.HBgM...",
    "type": "text",
    "text": "Hi, is the red one in stock?",
    "mediaId": null,
    "mimeType": null,
    "timestamp": "2026-10-27T10:15:00.000Z"
  },
  "referral": {
    "sourceType": "ad",
    "sourceId": "120210000000000",
    "sourceUrl": "https://fb.me/example",
    "headline": "Spring sale",
    "ctwaClid": "ARAk..."
  }
}

message.status

A message you sent changes status. Each time a message you sent is sent, delivered, read or fails. Statuses can arrive out of order; use timestamp.

  • messageId (string) Message id in the API.
  • conversationId (string) Conversation the message belongs to.
  • wamid (string or null) WhatsApp message id, once WhatsApp has accepted it.
  • status (string) sent, delivered, read or failed.
  • errorCode (string or null) WhatsApp error code when status is failed.
  • timestamp (string) ISO 8601 time of the status.
  • contact.id (string) Id of the recipient in the API.
  • contact.phone (string or null) Phone number with country code. Null for people who message through a WhatsApp username.
  • contact.bsuid (string or null) WhatsApp business-scoped user id (BSUID). Always present once the person has messaged you through WhatsApp; use it as the stable key when phone is null.
  • contact.waId (string or null) WhatsApp id (the phone number as digits), when WhatsApp shared it.
  • contact.username (string or null) WhatsApp username, when known.
  • contact.name (string or null) Display name.
{
  "messageId": "ckmsg1",
  "conversationId": "ckconv1",
  "wamid": "wamid.HBgM...",
  "status": "delivered",
  "errorCode": null,
  "timestamp": "2026-10-27T10:15:00.000Z",
  "contact": {
    "id": "ckcontact1",
    "phone": "+919876543210",
    "bsuid": "IN.13491208655302741918",
    "waId": "919876543210",
    "username": null,
    "name": "Asha"
  }
}

template.status

A template is reviewed. When Meta approves, rejects, pauses or otherwise changes the review status of one of your templates.

  • templateId (string or null) Template id in the API, when the template is known here.
  • name (string or null) Template name.
  • language (string or null) Template language code.
  • category (string or null) marketing, utility or authentication.
  • status (string) approved, rejected, pending, paused or disabled.
  • reason (string or null) Why Meta rejected it, when it did.
  • timestamp (string) ISO 8601 time we recorded the change.
{
  "templateId": "cktemplate1",
  "name": "order_update",
  "language": "en",
  "category": "utility",
  "status": "approved",
  "reason": null,
  "timestamp": "2026-10-27T10:15:00.000Z"
}

broadcast.completed

A broadcast finishes. Once every recipient of a broadcast has been processed. Delivery numbers can still grow afterwards as receipts arrive.

  • broadcastId (string) Broadcast id in the API.
  • name (string or null) Broadcast name.
  • templateId (string or null) Template the broadcast used.
  • completedAt (string) ISO 8601 time of completion.
  • totals.total (integer) Recipients in the broadcast.
  • totals.sent (integer) Recipients whose message WhatsApp accepted (includes delivered and read).
  • totals.delivered (integer) Delivered so far (includes read).
  • totals.read (integer) Read so far.
  • totals.failed (integer) Recipients whose message failed.
  • totals.skipped (integer) Recipients left out, for example because they opted out.
{
  "broadcastId": "ckbroadcast1",
  "name": "October offer",
  "templateId": "cktemplate1",
  "completedAt": "2026-10-27T10:15:00.000Z",
  "totals": {
    "total": 120,
    "sent": 118,
    "delivered": 110,
    "read": 64,
    "failed": 2,
    "skipped": 0
  }
}

contact.opted_out

A contact opts out. When someone sends STOP, an agent marks the contact as opted out, the API marks the contact as opted out, or a chatbot unsubscribe step runs. We stop all marketing messages to them.

  • contact.id (string) Id of the contact in the API.
  • contact.phone (string or null) Phone number with country code. Null for people who message through a WhatsApp username.
  • contact.bsuid (string or null) WhatsApp business-scoped user id (BSUID). Always present once the person has messaged you through WhatsApp; use it as the stable key when phone is null.
  • contact.waId (string or null) WhatsApp id (the phone number as digits), when WhatsApp shared it.
  • contact.username (string or null) WhatsApp username, when known.
  • contact.name (string or null) Display name.
  • contactId (string) Same as contact.id, kept for older integrations.
  • phone (string or null) Same as contact.phone, kept for older integrations.
  • optedOutAt (string) ISO 8601 time of the opt-out.
  • source (string) stop_keyword, dashboard, api (a compat API call set opt_in to false) or chatbot (an unsubscribe step in a chatbot flow).
{
  "contact": {
    "id": "ckcontact1",
    "phone": "+919876543210",
    "bsuid": "IN.13491208655302741918",
    "waId": "919876543210",
    "username": null,
    "name": "Asha"
  },
  "contactId": "ckcontact1",
  "phone": "+919876543210",
  "optedOutAt": "2026-10-27T10:15:00.000Z",
  "source": "stop_keyword"
}

conversation.status_changed

A conversation changes status. When a conversation moves between open, pending, solved and blocked, by an agent, the API, an automation or a customer reply that reopens it.

  • conversationId (string) Conversation id in the API.
  • contactId (string or null) Contact the conversation belongs to.
  • from (string or null) Previous status, when known.
  • to (string) New status.
  • changedBy (string or null) User id of the person who changed it. Null for automations and customer replies.
  • source (string) user when a person or API key changed it, system otherwise.
  • timestamp (string) ISO 8601 time of the change.
{
  "conversationId": "ckconv1",
  "contactId": "ckcontact1",
  "from": "open",
  "to": "solved",
  "changedBy": "ckuser1",
  "source": "user",
  "timestamp": "2026-10-27T10:15:00.000Z"
}

lead.created

A lead arrives. When a person fills in one of your Meta lead forms and the lead is stored.

  • leadId (string) Lead id in the app.
  • status (string) new at creation.
  • source (string) Always meta_lead_form for now.
  • formId (string or null) Meta lead form id.
  • campaignId (string or null) Meta campaign id the lead came from.
  • adId (string or null) Meta ad id the lead came from.
  • createdAt (string) ISO 8601 time the person submitted the form.
  • contact (object or null) The contact created or matched for this lead, when the form had a phone number.
  • contact.id (string) Id of the contact in the API.
  • contact.phone (string or null) Phone number with country code. Null for people who message through a WhatsApp username.
  • contact.bsuid (string or null) WhatsApp business-scoped user id (BSUID). Always present once the person has messaged you through WhatsApp; use it as the stable key when phone is null.
  • contact.waId (string or null) WhatsApp id (the phone number as digits), when WhatsApp shared it.
  • contact.username (string or null) WhatsApp username, when known.
  • contact.name (string or null) Display name.
  • fields (object) The form answers, as field name to text.
{
  "leadId": "cklead1",
  "status": "new",
  "source": "meta_lead_form",
  "formId": "5550001112223",
  "campaignId": "120210000000001",
  "adId": "120210000000002",
  "createdAt": "2026-10-27T10:15:00.000Z",
  "contact": {
    "id": "ckcontact1",
    "phone": "+919876543210",
    "bsuid": "IN.13491208655302741918",
    "waId": "919876543210",
    "username": null,
    "name": "Asha"
  },
  "fields": {
    "full_name": "Asha",
    "phone_number": "+919876543210",
    "city": "Pune"
  }
}

Verify (Node.js)

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, headers, rawBody) {
  const timestamp = headers["x-edwyz-timestamp"];
  const given = headers["x-edwyz-signature"];
  if (!timestamp || !given) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(given);
  return a.length === b.length && timingSafeEqual(a, b);
}

Verify (Python)

import hmac, hashlib, time

def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
    ts = headers.get("x-edwyz-timestamp", "")
    given = headers.get("x-edwyz-signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    msg = ts.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)

Always compute the signature over the raw bytes you received, before any JSON parsing, compare in constant time, and reject timestamps more than five minutes old.