Skip to content
WaaruDocs

Receive webhooks

Receive WhatsApp events and verify that each callback came from Waaru.

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.

Use the webhook management API to read settings, save a receiver, rotate a secret, send a test or change delivery state from your server.

Configure the receiver

  1. Choose the connected WhatsApp number.
  2. 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.
  3. Select the required Events and choose Create webhook.
  4. 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.
  5. Select Send test. Confirm that your receiver verifies the signature and accepts the webhook.test event.
  6. Choose callback coverage explicitly. Keep Logic Flow for observation and set observeAllOrigins: true to observe supported native origins. Select Your server only when your server should own autonomous replies. See the setup sequence.

Use Save webhook for updates. Re-read the revision after concurrent changes and inspect delivery health.

Answer the activation check

Before creating an endpoint, changing its URL, saving a paused or disabled endpoint, or explicitly resuming it, Waaru sends an unsigned HTTPS POST:

{
  "type": "webhook.verification",
  "apiVersion": "v1",
  "challenge": "RANDOM_CHALLENGE"
}

Return a 2xx response with exactly this JSON object. Echo the supplied challenge unchanged and include no other fields:

{ "challenge": "RANDOM_CHALLENGE" }

Treat the challenge only as activation verification, never as a customer event. It proves current reachability, not signature handling.

A failed check returns 502 webhook_verification_failed and preserves configuration. Unchanged active endpoints continue without rechecking. After saving, use Send test to verify a signed delivery.

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",
      "attribution": {
        "source": "meta_webhook",
        "actorKind": "contact",
        "actorId": "contact_example",
        "campaignId": null,
        "relatedMessageId": null
      }
    }
  }
}

Content depends on type. Attachments can be pending; read current messages/media. Text and read events are not guaranteed.

Verify the raw request

Waaru sends these headers:

HeaderMeaning
X-Waaru-Event-IdStable event ID for duplicate detection.
X-Waaru-TimestampDelivery signing time as Unix seconds.
X-Waaru-SignatureOne 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) || typeof secret !== "string" || !secret)
    return false;
  if (!Number.isFinite(nowSeconds)) return false;
  if (typeof timestamp !== "string" || !/^\d{1,12}$/.test(timestamp))
    return false;
  if (typeof signature !== "string" || signature.length > 512) 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)
      );
    });
}

Retain raw bytes in your framework. Reject invalid signatures, then parse JSON and validate its shape, expected number and id against 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 resultDelivery behavior
Any 2xxDelivery is acknowledged.
408, 429, 5xx, or retryable transport failureRetried with bounded backoff and jitter within the 24-hour delivery window.
404 or 410The webhook pauses. Fix the endpoint and explicitly resume it.
Other non-success responseThat 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 to Use Logic Flow stops autonomous API sends; scoped reads and explicitly opted-in all-origin observations can continue. Callback failure, disablement or health never changes the responder.

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.

Observe without changing the responder

Set observeAllOrigins: true explicitly on callback saves to receive supported fresh observations while retaining Logic Flow. Omission writes false. This grants no reply authority and never replays history. See origin coverage, attribution and media deadlines.

On this page