Receive and verify Developer API webhooks
Configure a Waaru callback URL, select message events, verify signed raw requests in Node.js, deduplicate delivery, and recover paused or failing webhooks.
Reviewed
Webhooks deliver inbound messages and outbound status changes to your server. Configure the callback for the exact WhatsApp number in Settings > Developer > Webhooks. Your receiver must verify the signature and record each event before acknowledging it.
Configure the receiver
- Choose the connected WhatsApp number.
- Under Send inbound and status events, enter a Callback URL. Use a public HTTPS hostname on port 443. Redirects and private network targets are blocked.
- Select the required Events and choose Create webhook.
- Copy the signing secret and store it on your receiver. It is separate from the API key and is shown once when created or rotated.
- Select Send test. Confirm that your receiver verifies the signature and accepts the
webhook.testevent. - Confirm Developer API is the active inbound handler before testing a real inbound message. See the setup sequence.
Use Save webhook for later changes. Review the displayed delivery state, last successful delivery, and recent failures. If a setting changed elsewhere, reload its current state before saving again.
Recognize the event envelope
Selected message event types are message.received, message.sent, message.delivered, message.read, and message.failed. A test uses webhook.test with data: { "test": true } instead of a message.
Example message event with illustrative identifiers:
{
"id": "event_example",
"type": "message.received",
"apiVersion": "v1",
"createdAt": "2026-09-06T09:00:00.000Z",
"instance": {
"id": "number_example",
"label": "Support",
"displayPhoneNumber": "+919876543210"
},
"data": {
"message": {
"id": "message_example",
"conversationId": "conversation_example",
"contactId": "contact_example",
"direction": "inbound",
"status": "received",
"type": "text",
"text": { "body": "Can you help with my order?" },
"occurredAt": "2026-09-06T09:00:00.000Z"
}
}
}Message content depends on its type. Inbound attachments can be pending while media becomes available. Use the message and media read routes when you need to check current state. Do not assume every event contains text or that every recipient produces a read event.
Verify the raw request
Waaru sends these headers:
| Header | Meaning |
|---|---|
X-Waaru-Event-Id | Stable event ID for duplicate detection. |
X-Waaru-Timestamp | Delivery signing time as Unix seconds. |
X-Waaru-Signature | One or more comma-separated v1= HMAC-SHA256 signatures. |
The signed input is the timestamp string, a period, and the exact raw request body. Use the signing secret as the HMAC key. Do not parse and reserialize JSON before verification: even whitespace changes the signed input.
This Node.js helper verifies raw bytes and accepts a five-minute delivery timestamp tolerance as a receiver policy. Keep your server clock synchronized. The event's createdAt can be older during a retry; verify the delivery header timestamp instead.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWaaruWebhook({
rawBody,
timestamp,
signature,
secret,
nowSeconds = Math.floor(Date.now() / 1000),
}) {
if (!Buffer.isBuffer(rawBody) || !secret) return false;
if (typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) return false;
if (typeof signature !== "string") return false;
const signedAt = Number(timestamp);
if (!Number.isSafeInteger(signedAt)) return false;
if (Math.abs(nowSeconds - signedAt) > 300) return false;
const digest = createHmac("sha256", secret)
.update(timestamp)
.update(".")
.update(rawBody)
.digest("hex");
const expected = Buffer.from(`v1=${digest}`);
return signature
.split(",")
.slice(0, 4)
.some((candidate) => {
const actual = Buffer.from(candidate.trim());
return (
actual.length === expected.length && timingSafeEqual(actual, expected)
);
});
}Configure your HTTP framework to retain the raw bytes for this route. Pass the header values and stored secret to the helper. Reject missing or invalid signatures. After verification, parse the JSON, check the expected event shape and number, and confirm that id matches X-Waaru-Event-Id.
Store the event ID and durably queue your work before returning a 2xx. If that event ID is already recorded, acknowledge it without running its business action again. Signature verification alone does not prevent processing the same signed event twice.
Acknowledge and recover delivery
Keep the callback response quick; the delivery timeout is 10 seconds. Process slower business work after durable acceptance. Return a success status only when your receiver has safely accepted the event.
| Receiver result | Delivery behavior |
|---|---|
Any 2xx | Delivery is acknowledged. |
408, 429, 5xx, or retryable transport failure | Retried with bounded backoff and jitter within the 24-hour delivery window. |
404 or 410 | The webhook pauses. Fix the endpoint and explicitly resume it. |
| Other non-success response | That delivery becomes terminal. Redirects are not followed. |
Repeated failures can show a degraded state. Exhausting the delivery window ends that delivery and pauses further work. Review the failure, repair the receiver, and use Resume webhook when appropriate. Resuming does not make an expired delivery fresh or provide an arbitrary historical replay API. Reconcile current conversations and message status when recovering from a gap.
Design for repeated deliveries and status reconciliation. Use event IDs for deduplication and message IDs for message state. Callback failure never hides the conversation from Inbox or starts Logic Flow automatically.
Rotate or disable the callback
Rotate secret shows a new signing secret. Update your receiver promptly. The previous secret remains valid for 24 hours, and deliveries during the overlap can contain signatures for both secrets. Accept a matching signature rather than comparing the entire comma-separated header with one value.
Disable webhook stops callback delivery. Switching the number to Use Logic Flow also stops new Developer API requests and callbacks. Coordinate either change with your team so inbound messages retain a clear owner.
Reconcile callback and current state
data.message.status describes the transition represented by that event. A later message read can already show a newer state. Keep event processing idempotent and do not regress a delivered or read record merely because an older callback arrives later. failureCode is included on message.failed events only.
Use message reads to reconcile the current record when needed.
Next: Handle API errors and retries or inspect conversations.
Read conversations, messages, and media
List Waaru Developer API conversations, paginate message history, read delivery status, and download permitted media using number-bound API keys.
Handle API errors, limits, and retries
Interpret Waaru Developer API error codes, rate-limit headers, admission failures, and uncertain send outcomes without duplicating customer messages.