Rabt WhatsApp CRM

API Reference

Webhooks

Webhooks push events to your own server the moment they happen, so you don't have to poll the API. RabtCRM sends a signed POST request to a URL you control whenever a message arrives or a delivery status changes.


Creating an Endpoint

Go to Settings → Developers and open the Webhooks section.

Developer settings

Add an endpoint with:

FieldDescription
URLYour HTTPS endpoint. Must be publicly reachable
EventsWhich events to receive — see below
ChannelLimit to one channel, or leave as All channels
ConnectionsOptionally restrict to specific connections

A signing secret is generated for you. Copy it — you'll need it to verify requests.


Events

EventFires when
message.receivedAn inbound message arrives on any subscribed channel
message.statusAn outbound message's delivery status changes

Not every channel reports delivery status:

Channelmessage.receivedmessage.status
WhatsApp — Baileys, Business, and Cloud APIyesyes
Facebook Messengeryesyes
Instagram DMyesyes
Telegramyesno
Chat Widgetyesno
Emailyesno

Subscribing to message.status on a Telegram, Chat Widget, or Email endpoint is valid, but that endpoint will never fire — those channels do not report delivery state.


Request Headers

Every delivery carries three headers:

HeaderMeaning
X-RabtCRM-Signaturesha256=<hex> HMAC of the raw body
X-RabtCRM-EventThe event name
X-RabtCRM-DeliveryA stable event id, identical across retries

Payload

Every payload uses the same envelope:

{
  "event": "message.received",
  "eventId": "evt_9f2c1a5b7d3e4f60",
  "teamId": 12,
  "channelType": "META-CLOUD",
  "sourceId": 99,
  "sourceName": "Main WhatsApp",
  "timestamp": "2026-08-29T10:15:04.221Z",
  "data": {
    "chatId": 32896,
    "remoteJid": "[email protected]",
    "messageId": "wamid.HBgMMjAx...",
    "fromMe": false,
    "contactName": "Ahmed",
    "messageType": "conversation",
    "text": "Hello, is this still available?",
    "media": null,
    "timestamp": "2026-08-29T10:15:03.000Z"
  }
}

When a file is attached, media is an object instead of null:

"media": {
  "url": "https://media.rabtcrm.com/...",
  "mimetype": "image/jpeg",
  "caption": "The blue one"
}

A message.status event carries a smaller data object:

{
  "event": "message.status",
  "eventId": "evt_41b8c07e9a2d5f13",
  "teamId": 12,
  "channelType": "META-CLOUD",
  "sourceId": 99,
  "sourceName": "Main WhatsApp",
  "timestamp": "2026-08-29T10:15:09.882Z",
  "data": {
    "chatId": 32896,
    "remoteJid": "[email protected]",
    "messageId": "wamid.HBgMMjAx...",
    "status": "delivered",
    "errorMessage": null
  }
}

Verifying the Signature

Compute an HMAC-SHA256 of the raw request body using your signing secret, and compare it to the X-RabtCRM-Signature header.

Important: hash the raw body bytes exactly as received. Parsing the JSON and re-serialising it will change the bytes and the signature will never match.

import crypto from 'crypto';

function verify(rawBody, header, secret) {
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
  // Constant-time compare — never use ===
  const a = Buffer.from(expected);
  const b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or '')

Responding

Return any 2xx status as quickly as you can. Anything else counts as a failure and triggers a retry.

Do your real work after responding — if your handler takes longer than 10 seconds, the request times out and is retried even though you received it.


Retries

A failed delivery is retried 5 times with growing gaps:

AttemptDelay after previous
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry6 hours

After 6 total attempts the delivery is marked failed and is not retried again.


Deduplicate on Delivery ID

X-RabtCRM-Delivery (and the matching eventId) stays the same across every retry of the same event. Store the ids you've processed and ignore repeats — the same event can legitimately reach you more than once, because delivery is at-least-once, not exactly-once.


Restrictions

For security, webhook URLs must be publicly routable. Private and internal addresses are rejected — localhost, private ranges such as 10.x, 192.168.x, 172.16–31.x, link-local addresses, and cloud metadata endpoints. Redirects are not followed.


Next Step

Send messages programmatically from your own application.

Sending Messages →