Webhooks: Receive Real-Time Event Notifications on Your External URL
Webhooks feature lets you receive real-time event notifications on your own server's endpoint URL. Whenever a relevant event happens in your WANotifier account including when a customer sends you a message, a contact is created, a contact is updated, our system can send an HTTP POST request with a JSON payload to the external URL you specify.
Setting Up a Webhook
- Go to Integrations page and click on Manage button under Webhooks.
- Navigate to Webhooks tab and click on Add New Webhook.
- Fill in:
- Name: a label to identify this webhook (e.g. "My CRM Webhook").
- URL: your HTTPS endpoint (must start with
https://). - Events: tick one or more events you want to receive.
- Signing Secret (optional): a strong random string. If set, every payload is signed with HMAC-SHA256 so you can verify the request came from WANotifier.
- Click Create Webhook. Use the toggle in the table to enable/disable.
You can edit, disable, or delete a webhook anytime from the same page.
Request Format
Every event sends a POST request with this shape:
POST https://your-server.com/webhook
Content-Type: application/json
User-Agent: WANotifier-Webhook/1.0
X-Notifier-Event: <event slug>
X-Notifier-Delivery-Id: <unique 20-char delivery id>
X-Notifier-Signature: <hex HMAC-SHA256 of body using your secret> # only if a secret is configured
{
"event": "<event slug>",
"timestamp": 1746035200,
"data": { ... event-specific data ... }
}
Headers
Header | Description |
|---|---|
| The event slug (e.g. |
| A unique 20-character ID for this specific delivery attempt. Stays the same across retries — use it to deduplicate. |
| HMAC-SHA256 hex digest of the raw request body using your signing secret. Only sent if you configured a secret. |
Verifying the signature
If you configured a signing secret, verify every incoming request:
// Node.js example
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
// PHP example
function verify($raw_body, $signature_header, $secret) {
$expected = hash_hmac('sha256', $raw_body, $secret);
return hash_equals($expected, $signature_header);
}
If verification fails, reject the request with 401.
Events
We currently support the following events:
message.received- a customer sends a message to your WhatsApp Business numbercontact.created- a new contact is added to your accountcontact.updated- an existing contact's details change
1. message.received
Fires when a customer sends a message to your WhatsApp Business number.
Sample JSON request
{
"event": "message.received",
"timestamp": 1746035200,
"data": {
"message": {
"to": "+15551234567",
"from": "+919876543210",
"direction": "in",
"wa_message_id": "wamid.HBgMOTE5...",
"context": "",
"message": { "body": "Hi, I need help" },
"message_type": "text",
"created_datetime": "2026-04-30 12:46:40"
},
"contact": {
"first_name": "Priya",
"last_name": "Sharma",
"whatsapp_number": "+919876543210",
"subscription": "subscribed",
"list": [{ "id": 12, "name": "Default" }],
"tag": [],
"attributes": {}
}
}
}
Notes
tois your business WhatsApp number;fromis the customer.message.messagecontacts message JSON received from WhatsAppmessage_typevalues:text,image,video,audio,document,sticker,button,interactive,location, etc.
2. contact.created
Fires when a new contact is added to your account via API, manual save, or automatically when a new customer messages you for the first time. This event will not fire in case of bulk imports.
{
"event": "contact.created",
"timestamp": 1746035201,
"data": {
"contact": {
"first_name": "Priya",
"last_name": "Sharma",
"whatsapp_number": "+919876543210",
"subscription": "subscribed",
"modified_time": "2026-04-30 12:46:40",
"list": [{ "id": 12, "name": "Default" }],
"tag": [],
"attributes": {
"company": "Acme Inc"
}
}
}
}
3. contact.updated
Fires when an existing contact's details change. Includes both the new state and the previous state so you can diff them.
This event will not fire in case of bulk imports. This event also does not fire if the update writes back identical data (e.g. the same incoming message updating the contact with no real change).
{
"event": "contact.updated",
"timestamp": 1746035202,
"data": {
"contact": {
"first_name": "Priya",
"last_name": "Sharma",
"whatsapp_number": "+919876543210",
"subscription": "subscribed",
"modified_time": "2026-04-30 13:02:11",
"list": [
{ "id": 12, "name": "Default" },
{ "id": 18, "name": "Mumbai" }
],
"tag": [{ "id": 44, "name": "Lead" }],
"attributes": {
"company": "Acme Inc",
"city": "Mumbai"
}
},
"previous_contact": {
"first_name": "Priya",
"last_name": "Sharma",
"whatsapp_number": "+919876543210",
"subscription": "subscribed",
"modified_time": "2026-04-30 12:46:40",
"list": [{ "id": 12, "name": "Default" }],
"tag": [],
"attributes": {
"company": "Acme Inc"
}
}
}
}
Responding to a Webhook
- Respond with
2xxas quickly as possible (under 10 seconds). We treat any 2xx as success. - Respond with
4xx(other than 408 / 429) if the request is malformed or you don't want to process it. We will not retry. - Respond with
5xx,408, or429if you have a transient issue. We will automatically retry (see Retries below). - We don't read response bodies for routing, your status code is what matters.
If your endpoint is slow, return 2xx immediately and process the payload asynchronously on your side.
Retries
If a delivery fails with a transient error (HTTP 5xx, 408, 429, or a network-level error), we automatically retry up to 3 times with the following delays:
Attempt | Delay after previous |
|---|---|
1 (initial) | — |
2 | 1 minute |
3 | 5 minutes |
4 | 15 minutes |
After 4 total attempts the delivery is marked failed. You can retry it manually from the Logs tab.
4xx responses other than 408 and 429 are treated as permanent failures and will not be retried automatically.
Idempotency
The X-Notifier-Delivery-Id header stays constant across all retries (auto and manual) for the same delivery. Use it to deduplicate on your side. If you see the same delivery ID twice, you can safely ignore the second one.
Logs
The Logs tab on the Webhooks page shows every delivery attempt with:
- Time, event, webhook, status (Pending / Success / Failed), attempt count, response code
- Filters: by webhook, status, event, or free-text search
- Click any row to expand to see full request payload, response body, error message, and timestamps
- A Retry button on failed rows to trigger a single manual retry
Use this when you suspect a webhook isn't being delivered, or to inspect what we sent.
Best Practices
- Always use HTTPS. Required by us, and prevents payloads from being inspected on the network.
- Verify the signature if you've configured a signing secret otherwise anyone who learns your URL can forge requests.
- Deduplicate on
X-Notifier-Delivery-Id, retries (and rare network duplicates) can otherwise cause double-processing. - Respond fast with
2xxand process asynchronously to stay well under the 10-second timeout. - Return
5xx(not4xx) for transient issues so retries kick in. Returning4xxwill mark the delivery as permanently failed. - Use unique secrets per environment (dev, staging, production) so a leak in one doesn't compromise others.
Updated on: 30/04/2026
Thank you!