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.

Add an endpoint with:
| Field | Description |
|---|---|
| URL | Your HTTPS endpoint. Must be publicly reachable |
| Events | Which events to receive — see below |
| Channel | Limit to one channel, or leave as All channels |
| Connections | Optionally restrict to specific connections |
A signing secret is generated for you. Copy it — you'll need it to verify requests.
Events
| Event | Fires when |
|---|---|
message.received | An inbound message arrives on any subscribed channel |
message.status | An outbound message's delivery status changes |
Not every channel reports delivery status:
| Channel | message.received | message.status |
|---|---|---|
| WhatsApp — Baileys, Business, and Cloud API | yes | yes |
| Facebook Messenger | yes | yes |
| Instagram DM | yes | yes |
| Telegram | yes | no |
| Chat Widget | yes | no |
| yes | no |
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:
| Header | Meaning |
|---|---|
X-RabtCRM-Signature | sha256=<hex> HMAC of the raw body |
X-RabtCRM-Event | The event name |
X-RabtCRM-Delivery | A 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:
| Attempt | Delay after previous |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 6 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.