Webhooks
Receive customer messages and delivery updates on your server.
Enigma sends an HTTPS POST to your URL when:
| Event | When |
|---|---|
inbound_message | A customer sends a message, including button taps and WhatsApp Flow replies. |
message_status | A message you sent becomes sent, delivered, read or failed. |
Set up
In the dashboard, open Webhooks and create a webhook with your URL. Enigma generates a
secret for signing; copy it and store it on your server. Use Test to send a sample
message_status event, and Regenerate secret if it leaks.
Payloads
inbound_message is the message content plus the sender's waId. type says which fields are present:
{
"type": "text",
"text": "Where is my order?",
"waId": "249912345678"
}message_status reports a message by its messageId (the wamid returned when you sent it):
{
"messageId": "wamid.HBgLMjQ5OTEyMzQ1Njc4FQIAERgS",
"waId": "249912345678",
"status": "failed",
"errorDetails": {
"code": 131047,
"title": "Re-engagement message",
"message": "More than 24 hours have passed since the customer last replied."
}
}errorDetails is present only on failed. See Errors.
Headers
| Header | Value |
|---|---|
X-Enigma-Event | inbound_message or message_status |
X-Enigma-Timestamp | Unix time in milliseconds when the request was signed |
X-Enigma-Signature | sha256= + hex HMAC-SHA256 of the timestamp and body |
User-Agent | Enigma-Webhook/1.0 |
Verify the signature
Verify every request before trusting it. The signature is HMAC-SHA256, keyed with your webhook secret, over:
<X-Enigma-Timestamp>.<raw request body>Use the raw body bytes exactly as received. Parsing the JSON and serializing it again can change spacing or key order and break the signature. Also reject requests whose timestamp is more than five minutes old, so a captured request cannot be replayed.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.ENIGMA_WEBHOOK_SECRET;
app.post('/enigma/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Enigma-Timestamp');
const received = (req.get('X-Enigma-Signature') ?? '').replace('sha256=', '');
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const fresh = Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
if (!fresh || !valid) return res.sendStatus(401);
const event = JSON.parse(req.body);
// handle req.get('X-Enigma-Event') and event
res.sendStatus(200);
});Responding
Return any 2xx status within 10 seconds. Do slow work after responding, for example by
putting the event on a queue. Enigma makes one delivery attempt per event, so make your
endpoint reliable and use conversation history to catch up after
an outage.