BotPulsar

Home/ Developers/Webhooks

Webhooks

Get notified when a message arrives or a delivery status changes

Subscribe an HTTPS endpoint to the events you care about; BotPulsar signs every delivery so you can verify it actually came from BotPulsar before you act on it.

Subscribe a public HTTPS endpoint to one or more event types, then verify the X-Botpulsar-Signature header on every delivery using the one-time signing secret shown when you create the subscription — the same HMAC-SHA256(secret, "{timestamp}." + raw_body) check on both sides, compared in constant time.

Setup

Subscribe an endpoint

  1. Operations → Developer → Create webhook subscription

    Give it a name, a public HTTPS endpoint URL, and the event types to send. Only public HTTPS destinations are accepted — no localhost, private IPs, or internal hostnames.

  2. Copy the signing secret immediately

    Shown once, as whsec_..., in the creation response. You'll need it to verify every delivery.

  3. Return a 2xx quickly

    Acknowledge the delivery fast and do slow work asynchronously — a delivery that keeps timing out gets retried, then eventually dead-lettered.

Payload

What arrives at your endpoint

Headers
Content-Type: application/json
User-Agent: BOTPULSAR-Webhooks/1.0
X-Botpulsar-Delivery: 7c2b1e4a-9f3d-4c8b-b1a0-5e6d7c8b9a0f
X-Botpulsar-Event: message.received
X-Botpulsar-Signature: v1=5f4dcc3b5aa765d61d8327deb882cf99...
X-Botpulsar-Timestamp: 1798623330
Body — message.received
{
  "api_version": "2026-07-01",
  "type": "message.received",
  "event_id": "9a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "delivery_id": "7c2b1e4a-9f3d-4c8b-b1a0-5e6d7c8b9a0f",
  "workspace_id": "3f9c7e2a-1b4d-4e8a-9c3f-2a1b4d4e8a9c",
  "created_at": "2026-08-29T10:15:30Z",
  "data": {
    "message_id": "...",
    "conversation_id": "...",
    "contact_id": "...",
    "channel_account_id": "...",
    "direction": "inbound",
    "content_type": "text",
    "contact_created": false,
    "first_conversation": false
  }
}

delivery_id in the body is the same value as the X-Botpulsar-Delivery header — use whichever's convenient as your idempotency key. A retried delivery reuses the same ID.

Security

Verify the signature

Recompute the HMAC over the timestamp and the exact raw request body — not a re-serialized copy of the parsed JSON, which can reorder keys or change whitespace and break the match.

Python
import hashlib
import hmac
import time

def verify_botpulsar_signature(secret: str, timestamp: str, raw_body: bytes, signature_header: str) -> bool:
    # Reject stale deliveries so a captured request can't be replayed later.
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode("ascii"),
        f"{timestamp}.".encode("ascii") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    received = signature_header.removeprefix("v1=")
    return hmac.compare_digest(expected, received)
Node.js
const crypto = require('crypto');

function verifyBotpulsarSignature(secret, timestamp, rawBody, signatureHeader) {
  // Reject stale deliveries so a captured request can't be replayed later.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody) // the raw request body, not JSON.parse'd and re-stringified
    .digest('hex');
  const received = signatureHeader.replace(/^v1=/, '');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

Use secret as the whsec_... value from creating (or rotating) the subscription. Rotating replaces it immediately — swap your verification code over before the old secret stops being accepted.

Events

Every event type

EventFires when
message.received An inbound WhatsApp message arrived.
message.outbound.submitted A message you sent was accepted by WhatsApp for delivery.
message.delivery_status.changed A sent message moved to sent, delivered, read, or failed.
message.outbound.failed A message you sent could not be delivered.
message.media.ready Media attached to a message finished processing and can be fetched.
conversation.created A new conversation was opened with a contact.
conversation.status.changed A conversation moved between open, closed, or blocked.
contact.created A new contact was created.
contact.field.changed A custom field value on a contact changed.
contact.tag.changed A tag was added to or removed from a contact.
FAQ

Common questions

What happens if my endpoint is down when an event fires?

Delivery retries transient HTTP and network failures with bounded backoff. A delivery that keeps failing is dead-lettered rather than retried forever — check `webhook-deliveries/{id}/replay/` from the dashboard's Developer area to resend it once your endpoint is back.

How do I avoid processing the same event twice?

Store `X-Botpulsar-Delivery` (or the payload's `delivery_id` — they're the same value) for each delivery you've handled, and skip anything you've already seen. Retries reuse the same delivery ID.

Why verify over timestamp + body instead of just the body?

Binding the timestamp into the signed material stops a captured request from being replayed later. Reject any timestamp more than a few minutes old even if the signature itself is valid.

Does BotPulsar follow redirects on my endpoint?

No. Delivery follows no redirects and re-validates public DNS before every attempt, which is also why the endpoint must be a public HTTPS URL — not localhost, a private IP, or an internal hostname.

Start on WhatsApp today — from ₹300/month

Unlimited team members on every plan. 14-day trial, no setup fee, no annual lock-in.