Wazzap AI API Documentation - Webhooks
Table of contents
- Introduction
- Webhook setup
- Supported event types
- Delivery format
- Signature verification (HMAC)
- Reliability and failure handling
- Webhook server examples
- Best practices
- Troubleshooting
Introduction
Webhooks let you receive your agent's incoming WhatsApp messages in real time on your own server, without polling the API.
Every time your agent receives a message, Wazzap AI sends a signed POST request to the URL you configured.
Key features
- Real-time delivery of incoming messages
- Every delivery signed with HMAC-SHA256 (
X-Wazzap-Signatureheader) - URL validated with a test ping at registration
- Automatic disabling after repeated failures
- One webhook per API key, bound to the key's agent
Typical use cases
- Sync WhatsApp conversations into your CRM
- Trigger workflows (Zapier, Make, n8n…) when a message arrives
- Build your own support interface on top of Wazzap AI
- Log all incoming messages for analytics
Webhook setup
From the dashboard
- Go to Settings → API Keys
- On the API key you want, open the Webhook section
- Enter your server's HTTPS URL
- Wazzap AI immediately sends a validation ping (
webhook.test) — your server must reply with a 2xx status for the URL to be saved - Copy the HMAC secret shown: you'll use it to verify each delivery's signature
URL constraints
| Constraint | Detail |
|---|---|
| Protocol | https:// only (http rejected) |
| Network | Private/local addresses are rejected (localhost, 127.x, 10.x, 192.168.x, 172.16-31.x, 169.254.x) |
| Validation | The URL must answer the test ping with 2xx, otherwise it is not saved |
| Timeout | 10 seconds per delivery |
The validation ping
When you register the URL, your server receives this payload:
{
"event": "webhook.test",
"apiKeyId": "cm9xxx...",
"agentId": "cm9yyy...",
"data": {
"message": "Wazzap validation ping — reply with a 2xx status to validate this webhook.",
"timestamp": "2026-07-06T15:00:00.000Z"
}
}
Simply reply 200 OK (your response body is ignored).
Supported event types
| Event | Trigger | Description |
|---|---|---|
webhook.test |
URL registration / test from the dashboard | Validation ping. Reply 2xx. |
message.received |
Incoming WhatsApp message received by your agent | A contact sent a message to your connected WhatsApp number. |
⚠️ What does NOT trigger a webhook
- Your own sends via the
/api/wazzap/send-messageAPI - Messages typed manually from the connected phone (
fromMemessages) - AI-generated replies from your agent
- Read receipts, reactions and status updates
The webhook is intentionally limited to incoming messages: that's the event you need to react to your contacts. More event types may be added later — route on the payload's event field and safely ignore types you don't handle.
Delivery format
Request headers
Every delivery is a POST with these headers:
POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Wazzap-Webhook/1.0
X-Wazzap-Signature: sha256=3f5a8c…
message.received payload — text message
{
"event": "message.received",
"agentId": "cm9yyy...",
"organizationId": "cm9zzz...",
"data": {
"conversationId": "cmr9aaa...",
"contactId": "cmr9bbb...",
"phoneNumber": "33612345678",
"contactName": "John Doe",
"message": "Hi, I'd like some information",
"messageType": "text",
"fromMe": false,
"media": null,
"transcription": null,
"description": null,
"messageId": "3EB0A5B4E7CB234BC78A48",
"timestamp": "2026-07-06T15:18:55.000Z"
}
}
message.received payload — voice message (audio)
Voice messages are automatically transcribed (Whisper). The transcription lands in message AND transcription; the audio file (hosted on our public CDN) is in media.url.
{
"event": "message.received",
"agentId": "cm9yyy...",
"organizationId": "cm9zzz...",
"data": {
"conversationId": "cmr9aaa...",
"contactId": "cmr9bbb...",
"phoneNumber": "33612345678",
"contactName": "John Doe",
"message": "Hi, is the product still available?",
"messageType": "audio",
"fromMe": false,
"media": {
"url": "https://pub-xxxx.r2.dev/wazzap-v2.5/audio/1783351406671-xxx.mp3",
"type": "audio",
"mimeType": "audio/ogg; codecs=opus"
},
"transcription": "Hi, is the product still available?",
"description": null,
"messageId": "false_963835624366@lid_3A4355A4022B54D3A211",
"timestamp": "2026-07-06T15:23:14.000Z"
}
}
💳 Cost: each successful transcription consumes 1 credit from your organization. If your balance is zero, transcription is skipped (the voice note is still delivered with its media.url, transcription is null). You can disable transcription at any time with the "Transcribe voice notes" switch in the webhook settings (Settings → API Keys → Webhook).
⚠️ If transcription fails (inaudible audio, service unavailable…), no credit is charged and the delivery still happens: transcription is null, message contains any text/caption (usually "" for a voice note), and media.url lets you fetch the audio and process it yourself.
message.received payload — image
Incoming images are analyzed by AI vision: the generated description lands in description. message contains the caption typed by the contact (often empty).
{
"event": "message.received",
"agentId": "cm9yyy...",
"organizationId": "cm9zzz...",
"data": {
"conversationId": "cmr9aaa...",
"contactId": "cmr9bbb...",
"phoneNumber": "33612345678",
"contactName": "John Doe",
"message": "Is this the right model?",
"messageType": "image",
"fromMe": false,
"media": {
"url": "https://pub-xxxx.r2.dev/wazzap-v2.5/images/1783351406671-xxx.jpg",
"type": "image",
"mimeType": "image/jpeg"
},
"transcription": null,
"description": "Photo of a pair of white sneakers with red laces…",
"messageId": "false_963835624366@lid_3A4355A40BB54D3A211",
"timestamp": "2026-07-06T15:25:02.000Z"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
event |
string |
Event type (message.received) |
agentId |
string |
ID of the agent that received the message |
organizationId |
string |
Your organization ID |
data.conversationId |
string |
Conversation ID in Wazzap AI |
data.contactId |
string |
Contact ID in Wazzap AI |
data.phoneNumber |
string |
Sender's number (without the +) |
data.contactName |
string | null |
Contact name (if known) |
data.message |
string |
Clean message text: text or caption; transcription for a voice note when it succeeded; "" for a silent media |
data.messageType |
string |
text | image | audio | video | document |
data.fromMe |
boolean |
Message direction: false = sent by the contact (incoming). Always false for message.received — explicit field, ready for future event types |
data.media |
object | null |
Attached media: { url, type, mimeType }. null for a text message. url points to our public CDN (or the source URL if the CDN upload failed) |
data.transcription |
string | null |
Audio only: automatic transcription of the voice note, null if it failed or wasn't attempted |
data.description |
string | null |
Image only: AI-vision generated description, null if not analyzed |
data.messageId |
string | null |
WhatsApp message ID (use it as an idempotency key) |
data.timestamp |
string |
Message send time (ISO-8601) |
Your response
Reply with a 2xx status (ideally 200) as fast as possible — process the message asynchronously if your logic is slow. Any non-2xx response or a timeout (>10s) counts as a delivery failure.
Signature verification (HMAC)
Every delivery is signed with your webhook's HMAC secret:
X-Wazzap-Signature: sha256=<hex(HMAC-SHA256(secret, raw_request_body))>
⚠️ Always verify the signature before processing a delivery — it's your guarantee the request really comes from Wazzap AI. Compute the HMAC over the raw body (the exact bytes received, before any JSON parsing).
Reliability and failure handling
| Rule | Value | Detail |
|---|---|---|
| Delivery timeout | 10s | Beyond that, the delivery counts as failed |
| Tolerated consecutive failures | 20 | The counter (webhookFailCount) increments on each failure |
| After 20 consecutive failures | Webhook disabled | Re-enable it from the dashboard once your endpoint is fixed |
| After one success | Counter reset to zero | A single success clears the failure streak |
⚠️ No automatic redelivery: a failed delivery is not replayed. If your endpoint was down, recover missed messages from your Wazzap AI dashboard.
Webhook server examples
Test quickly without writing code
To validate your setup without a server, use a webhook inspection service (webhook.site, webhooklens.cloud, requestbin…): paste the generated URL into the dashboard, send a WhatsApp message to your agent from another phone, and watch the delivery arrive.
Forward to Slack
app.post('/wazzap-webhook', async (req, res) => {
if (!verifySignature(req)) return res.status(401).end();
res.status(200).json({ ok: true });
const { event, data } = req.body;
if (event !== 'message.received') return;
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `💬 *${data.contactName || data.phoneNumber}* : ${data.message}`
})
});
});
Auto-reply via the API
app.post('/wazzap-webhook', async (req, res) => {
if (!verifySignature(req)) return res.status(401).end();
res.status(200).json({ ok: true });
const { event, data } = req.body;
if (event !== 'message.received') return;
// Example: acknowledgement outside business hours
const hour = new Date().getHours();
if (hour < 9 || hour >= 18) {
await fetch('https://api21.wazzap.ai/api/wazzap/send-message', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WAZZAP_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
phoneNumber: '+' + data.phoneNumber,
message: 'Thanks for your message! We will reply from 9am.',
disableAiResponse: true
})
});
}
});
Best practices
1. Reply fast, process later
Your endpoint has 10 seconds to respond. Acknowledge immediately (200) then process asynchronously (queue, worker…). Slow synchronous processing = timeouts = webhook disabled after 20 failures.
2. Always verify the signature
Without HMAC verification, anyone who knows your URL can inject fake messages.
3. Be idempotent
Use data.messageId as an idempotency key: if you receive the same messageId twice, process it only once.
4. Ignore unknown events
Route on the event field and reply 200 to types you don't handle — new types may appear.
5. Monitor your endpoint
An endpoint failing silently = webhook disabled after 20 failures. Monitor its availability and check the webhook status in the dashboard regularly.
Troubleshooting
I'm not receiving any webhooks:
- Check the webhook is enabled in the dashboard (it may have been disabled after 20 failures)
- Remember only incoming messages trigger the webhook — not your API sends nor messages typed from the phone
- Check your WhatsApp agent is connected
- Test with an inspection service (webhook.site) to isolate whether the issue is on Wazzap's side or your server's
The URL is rejected at registration:
- The URL must be
https://and publicly reachable (no localhost/private IP) - Your server must answer the
webhook.testping with 2xx within 10s
Signature verification fails:
- Compute the HMAC over the request's raw body, not re-serialized JSON
- Check you're using the right secret (viewable again in the dashboard)
- The header format is
sha256=<hex>— compare the full string
Contact
For any question:
- Check this documentation first
- Contact us at contact@Wazzap.ai
