Webhooks and events
The OpenWA delivery envelope, HMAC signature check, and the events the engine consumes.
Delivery
For each session the engine registers one webhook with the gateway (events, URL, and a shared HMAC secret). OpenWA then POSTs to /webhooks/openwa:
{
"event": "message.received",
"timestamp": "...",
"sessionId": "...",
"idempotencyKey": "...",
"deliveryId": "...",
"data": {}
}The URL is OPENWA_WEBHOOK_URL when set, otherwise PUBLIC_URL/webhooks/openwa. In local development the gateway runs in Docker and reaches the host through http://host.docker.internal:8787/webhooks/openwa. OpenWA's SSRF guard blocks private addresses, so docker-compose.yml allow-lists that host through SSRF_ALLOWED_HOSTS. In production use the engine's public https URL.
Signature
OpenWA signs the exact body it sends: X-OpenWA-Signature: sha256=<hex>, where the hex is HMAC-SHA256 of the raw body with OPENWA_WEBHOOK_SECRET. The engine verifies it with crypto.subtle.verify (constant-time) before parsing. A missing or invalid signature returns 401 and nothing is stored.
Events consumed
| Event | Handling |
|---|---|
message.received | Inbound message from a customer or group member. Own messages are skipped here. |
message.sent | A message the account sent. If the engine did not send it, staff replied from the phone and the conversation switches to human mode. |
message.ack | Delivery and read updates, applied monotonically (sent, delivered, read). |
message.failed | Marks the message failed. |
session.status, session.authenticated, session.disconnected | Update the connection and session status. See Sessions. |
session.restriction | Recorded in channel_event for review. |
group.join, group.leave, group.update | Recorded, upsert the group conversation, and end a leaving member's rights to group memory. |
Any other event is acknowledged and ignored.
Processing
- Verify the signature on the raw body.
- Store the payload under
raw/openwa/<date>/<uuidv7>.jsonin R2. Inline media is replaced with a marker first. A signed but malformed body goes underraw/openwa/invalid/and is acknowledged so OpenWA stops retrying. - Enqueue one QStash job per event with a deduplication id:
msg:<sessionId>:<messageId>,own:...,st:<sessionId>:<messageId>:<status>, orev:<idempotencyKey>. OpenWA retries with the same idempotency key, so redelivery is safe. Enqueue failure returns 5xx so OpenWA redelivers. - Message jobs carry a flow key per sender (per group for groups) with parallelism 1. Session events use a per-session flow key so status changes apply in order. Own-message echoes are delayed 8 seconds so the send job can record its outbound row first.