Send WhatsApp events to your server with webhooks
WACRM can POST a signed event to your own server the moment a WhatsApp message arrives or a delivery status changes, so your systems react without polling the API. This guide walks through adding an endpoint and verifying signatures in Workspace, the same flow through the API, and the honest limits of what ships today.
By the WACRM team Updated
What you'll learn
- What you need before you start
- Add a webhook endpoint in Workspace
- Verify the signature on your server
- Send a test, read Deliveries, and redeliver a failed one
- Rotate the secret
- Do the same thing through the API
- What works today vs. not yet
Outbound webhooks let WACRM push a WhatsApp event to a URL you control, signed so you can confirm it really came from WACRM, instead of your server polling the API for changes. This guide covers the two places that manage the same webhook endpoints — Workspace’s Settings → Apps & Connectors → Webhooks screen and the Core API directly — and says plainly which events, retries and limits exist today.
What you need before you start
- A WACRM Workspace account with at least one project, on the Advanced plan or higher (owner access, to manage webhooks from Workspace).
- A server you control that can receive an HTTPS
POSTrequest and respond with a 2xx status. - For the API steps only:
curl(or any HTTP client that can set a header), and an API key with thewa_webhooks:read/wa_webhooks:writescopes from Developer / API & MCP.
Add a webhook endpoint in Workspace
- Open Settings → Apps & Connectors → Webhooks. This screen is owner-only — a team member without owner access won’t see a way to add or change an endpoint here.
- Click Add endpoint.
- Enter your server’s Endpoint URL — it must start with
https://, and it’s refused if it resolves to a private, loopback, link-local or other reserved address (checked again at delivery time, so a hostname that gets repointed after you create it is still caught). - Tick the Events to send you want:
message.inbound(a WhatsApp message arrived) and/ormessage.status(a message’s delivery status changed). These are the only two events available today. - Click Create endpoint.
A Your new webhook signing secret dialog opens immediately after create, showing the secret exactly once — copy it now. WACRM never stores or logs a readable copy of it, and can’t show that same secret again; if you lose it, rotate it for a new one (see below).
Verify the signature on your server
Every delivery — a real event, a redelivery or a test send — carries an X-WACRM-Signature header shaped like
t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<t>.<raw body>", signed with your endpoint's secret>. Recompute the
same HMAC over the exact raw request body and compare it to v1 before trusting the payload; never trust the
body alone. This is the same signed-webhook pattern most webhook providers use (see the HMAC and webhooks
sources below) — with placeholders standing in for your own secret, never a real one:
const crypto = require('crypto');
// header: the raw X-WACRM-Signature value; rawBody: the exact bytes WACRM sent,
// not a re-serialized JSON.parse(body) — whitespace differences break the HMAC.
function isValidWebhookSignature(header, rawBody, endpointSecret) {
const [tPart, v1Part] = header.split(',');
const timestamp = tPart.split('=')[1];
const expected = crypto
.createHmac('sha256', endpointSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return v1Part === `v1=${expected}`;
} Send a test, read Deliveries, and redeliver a failed one
- From the endpoint’s row, click Send test. This sends exactly one signed
webhook.testevent through the same delivery path, SSRF check and signature as a real event — useful for confirming your server validates the signature correctly before you wire up real traffic. It’s capped at 5 test sends per endpoint per minute; a 6th within the same minute is rejected. - Click Deliveries to open the list of everything sent to this endpoint — event, status, HTTP status, attempts and the last attempt’s time, newest first. Deliveries are kept for 30 days and then removed.
- For a delivery marked Failed, click Redeliver. This resends the exact same payload as a brand-new delivery — the original failed row is kept as-is, so you still have a record of what happened the first time.
An endpoint that’s turned off (Active switched off) refuses both Send test and Redeliver outright — turn it back on first.
Rotate the secret
If your secret leaks, or you just want a fresh one, click Rotate secret on the endpoint’s row. The same one-time reveal dialog opens with the new secret — copy it immediately, since the old secret stops verifying deliveries right away, with no overlap window for in-flight signatures. Update your server with the new secret before rotating, or your verification will start failing the moment you confirm.
Do the same thing through the API
Everything above is also reachable directly, scoped to wa_webhooks:read/wa_webhooks:write — tick both under
Restrict what this key can do (advanced) → WhatsApp CRM scopes when you mint a key in Developer / API &
MCP. Neither scope is granted by default to any key, since an endpoint receives your project’s live message
content.
| Action | Request |
|---|---|
| Create an endpoint | POST /v1/webhook-endpoints |
| List your endpoints | GET /v1/webhook-endpoints |
| Change the URL, events or active flag | PATCH /v1/webhook-endpoints/{endpoint} |
| Rotate the secret | POST /v1/webhook-endpoints/{endpoint}/rotate-secret |
| Send a test delivery | POST /v1/webhook-endpoints/{endpoint}/test |
| List deliveries | GET /v1/webhook-endpoints/{endpoint}/deliveries |
| Redeliver one | POST /v1/webhook-endpoints/{endpoint}/deliveries/{delivery}/redeliver |
curl -s -X POST https://api.wacrm.ai/v1/webhook-endpoints \
-H "Authorization: Bearer sk_live_your_scoped_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-server.com/webhook", "events": ["message.inbound", "message.status"]}' {
"data": {
"id": "7e9b9b7a-...-endpoint-uuid",
"url": "https://your-server.com/webhook",
"events": ["message.inbound", "message.status"],
"active": true,
"created_at": "2026-11-08T09:00:00+00:00",
"secret": "whsec_your_one_time_secret"
}
} secret is only ever present in this create response and in a rotate-secret response — every other call,
including re-fetching the same endpoint, omits it entirely. A project can have at most 20 endpoints; creating a
21st answers 409 {"error": {"code": "endpoint_limit_reached"}}. PATCH takes whichever of url/events/
active you’re changing (at least one is required) and never returns a secret. A redelivered delivery or a
test send against an endpoint with active: false answers 409 {"error": {"code": "endpoint_inactive"}}
before anything else runs; a 6th test send in the same minute answers 429 {"error": {"code": "rate_limited"}}.
Note
The same screen also works from a plain Workspace session, with no API key — the Settings UI calls a
session-authenticated route behind the scenes, still owner-only. A non-owner team member gets the same
403 either way.
What works today vs. not yet
| What you want to do | Works today? |
|---|---|
| React to a new inbound WhatsApp message | Yes — subscribe to message.inbound |
| React to a delivery status change (sent, delivered, read, failed) | Yes — subscribe to message.status |
| Get notified about a conversation-level event (assignment, tag, label) | Not yet — only the two message events exist |
| Point an endpoint at an internal or private address | No — https only; private, loopback, link-local and reserved addresses are refused, even if a hostname’s DNS changes after creation |
| Look up a delivery from more than 30 days ago | No — deliveries are kept 30 days, then removed |
| Send more than 5 test deliveries to one endpoint in a minute | No — the 6th is rate-limited |
| Manage webhooks in Workspace as a non-owner team member | No — owner-only |
| Manage webhooks on the Premium plan | No — included from the Advanced plan up; check your plan in Settings if you’re not sure |
Ready to try this against your own project? Request early access to get a Workspace, or see pricing first if you’re still comparing plans. For more how-tos like this one, browse the full guides list.
Talk to an expert
A person from the WACRM team replies.
Ask David a question about whatsapp webhooks: send events to your server.
Replies within one working day, usually sooner on WhatsApp.
Frequently asked questions
Do I need a paid plan to use webhooks?
Webhooks are included from the Advanced plan up — the Premium plan doesn't have them. If you're not sure which plan your project is on, check your plan in Settings.
Can I subscribe to a conversation-level event, like a new assignment or tag?
Not yet. Only two events exist today — message.inbound (a WhatsApp message arrived) and message.status (a message's delivery status changed). Conversation-level events aren't available yet, through Workspace or the API.
What happens if my server is down when WACRM tries to deliver an event?
WACRM retries up to 3 times with backoff (30 seconds, 2 minutes, then 10 minutes) before marking the delivery failed. You can see every attempt in that endpoint's Deliveries list and click Redeliver once your server is back, which resends the same payload as a new delivery.
Who can manage webhooks in Workspace?
Only the project owner. A team member without owner access won't see a way to add, edit, rotate or delete a webhook endpoint from Settings.
Want this set up for you?
Quick Setup connects your number with coexistence in 48 hours, done by a person.