Webhooks
Get data out of MYWA in real time. When something happens — a new lead, an incoming message, a delivery update — MYWA POSTs it to an HTTPS endpoint you control, signed so you can trust it.
Configured in the app, not the API
Events
An Outbound Webhook can send any of these events. You pick which ones when you set it up, so your endpoint only receives what it needs.
| Field | Type | Description |
|---|---|---|
lead.created | event | A new lead — from an ad, a campaign, or a first message. |
message.received | event | A customer sent you a message. |
message.status | event | A message you sent was sent, delivered, read, or failed. |
contact.created | event | A new contact was added. |
contact.opted_out | event | A contact unsubscribed from messages. |
conversation.status_changed | event | A conversation moved to another status, e.g. open to resolved. |
conversation.quality_changed | event | A conversation’s quality score changed. |
The payload
Each delivery is a POST with the same JSON envelope: event, lead (filled for lead.created, null otherwise), data with the event’s own fields, and occurredAt. A new lead looks like this:
{
"event": "lead.created",
"lead": {
"event": "lead.created",
"name": "Jane Doe",
"phone": "+15551234567",
"email": "jane@example.com",
"campaign": "Summer Sale",
"sourceType": "ad",
"source": "campaign",
"tags": ["vip"],
"leadAt": "2026-07-18T12:00:00.000Z",
"conversationId": "cnv_…",
"contactId": "cts_…",
"organizationId": "org_…"
},
"data": { "leadSourceType": "campaign", "leadSource": "cmp_…", "referral": null },
"occurredAt": "2026-07-18T12:00:00.000Z"
}And a delivery update. status is one of sent, delivered, read, or failed; workflowRunId is set when an automation sent the message:
{
"event": "message.status",
"lead": null,
"data": {
"messageId": "msg_8f1a…",
"status": "delivered",
"errorCode": null,
"workflowRunId": null
},
"occurredAt": "2026-07-18T12:00:04.000Z"
}Two headers accompany it: X-Wasend-Event and X-Wasend-Signature.
Verifying the signature
X-Wasend-Signature is an HMAC-SHA256 of the raw request body using the integration’s signing secret (reveal it in the integration’s settings). Compute the same HMAC and compare in constant time. Always hash the raw bytes — re-serializing parsed JSON can change the output.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.WASEND_WEBHOOK_SECRET;
// Capture the raw body for signature verification
app.post('/webhooks/wasend', express.raw({ type: '*/*' }), (req, res) => {
const signature = req.header('X-Wasend-Signature') ?? '';
const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return res.status(401).end();
const payload = JSON.parse(req.body.toString('utf8'));
if (payload.event === 'lead.created') syncLeadToCrm(payload.lead); // your code
res.json({ received: true });
});From an automation
An automation can also call your endpoint mid-flow with the “Send to a webhook” step. It sends the event workflow.action with the automation, the run, the contact, and the run’s vars. Point the step at one of your Outbound Webhooks and it is signed the same way, with the same X-Wasend-Signature header, so the verification code above works unchanged.
{
"event": "workflow.action",
"workflow": { "id": "wf_9f2", "name": "Order follow-up" },
"run": { "id": "run_7c1", "nodeId": "node_4" },
"contact": { "id": "cts_31a…", "waPhone": "+15551234567" },
"conversation": { "id": "cnv_88b…" },
"vars": { "hook": { "orderId": "A-1049" } },
"sentAt": "2026-08-27T09:20:00.000Z"
}Delivery & retries
- Respond with a
2xxquickly. Non-2xx responses are retried with backoff. - Do slow work (CRM sync, emails) after acknowledging, ideally on a queue.
- Every attempt — success or failure — is recorded in the integration’s delivery history in Settings → Integrations, so you can see exactly what was sent and why anything failed.