Webhooks
Subscribe to server-side chat events for moderation, analytics, CRM sync, and offline push notifications. Events are delivered as signed HTTPS POSTs to your endpoint.
Real-time push
HTTP callbacks within hundreds of milliseconds of chat activity — no polling required.
Message events
chat.message.created for every user and system message across your channels.
HMAC signatures
Verify X-Sendexa-Signature with your webhook secret before trusting any payload.
Automatic retries
Failed deliveries retry up to 5 times with exponential backoff.
<500ms
event → POST
5
exponential backoff
3
message · member · channel
SHA256
HMAC header
Configure in the dashboard
Always verify signatures
X-Sendexa-Signature. Reject unsigned or invalid requests with 401.chat.message.createdA message was posted to a channel (client SDK or server REST).
chat.member.joinedA user was added to a channel’s membership list.
chat.channel.createdA new channel was created for your business.
Delivery Envelope
Every webhook POST uses the same envelope. Event-specific data lives under data. Respond with 2xx within 10 seconds or the attempt is marked failed and retried.
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | Unique event delivery id (evt_...). Use for idempotency. |
| type | string | required | One of chat.message.created | chat.member.joined | chat.channel.created |
| createdAt | string | required | ISO 8601 timestamp when the event occurred. |
| businessId | string | required | Your Sendexa business id. |
| data | object | required | Event payload — shape depends on type. |
{"id": "evt_01HZYC1K","type": "chat.message.created","createdAt": "2026-07-12T10:05:12.500Z","businessId": "BU-VOIF86X7","apiVersion": "v1","data": { }}
Fired for every message accepted into history — including posts from @sendexa/chat-sdk and POST .../messages. Ideal for push notifications to offline members and abuse scanning.
| Field | Type | Required | Description |
|---|---|---|---|
| data.message.id | string | required | Message id (msg_...). |
| data.message.channelId | string | required | Channel the message belongs to. |
| data.message.userId | string | optional | Author user id; null/omitted for pure system posts. |
| data.message.body | string | required | Message text. |
| data.message.type | string | required | text or system. |
| data.message.seq | number | required | Per-channel sequence number. |
| data.channel.memberIds | string[] | required | Current members — useful for fan-out push. |
{"id": "evt_01HZYC1K","type": "chat.message.created","createdAt": "2026-07-12T10:05:12.500Z","businessId": "BU-VOIF86X7","apiVersion": "v1","data": {"message": {"id": "msg_01HZYB8P","channelId": "ch_01HZYA3N","userId": "user_01HZX9K2M","body": "Hi — I need help with my order.","type": "text","metadata": {"clientMessageId": "local-uuid-1"},"createdAt": "2026-07-12T10:05:12.441Z","seq": 1},"channel": {"id": "ch_01HZYA3N","type": "dm","name": "Support · Ama","memberIds": ["user_01HZX9K2M", "agent_support_01"]}}}
Emitted when a user id is added to a channel — at creation time for initial members and later when membership expands. Use this to grant access in your product UI or invite agents.
{"id": "evt_01HZYC2M","type": "chat.member.joined","createdAt": "2026-07-12T10:01:00.120Z","businessId": "BU-VOIF86X7","apiVersion": "v1","data": {"channelId": "ch_01HZYA3N","userId": "agent_support_01","displayName": "Support Agent","joinedAt": "2026-07-12T10:01:00.100Z","addedBy": "system","channel": {"id": "ch_01HZYA3N","type": "dm","name": "Support · Ama","memberIds": ["user_01HZX9K2M", "agent_support_01"]}}}
Fired once when POST /v1/chat-sdk/channels succeeds. Pair with member joined events if you need per-user side effects for the initial roster.
{"id": "evt_01HZYC0A","type": "chat.channel.created","createdAt": "2026-07-12T10:01:00.050Z","businessId": "BU-VOIF86X7","apiVersion": "v1","data": {"channel": {"id": "ch_01HZYA3N","type": "dm","name": "Support · Ama","memberIds": ["user_01HZX9K2M", "agent_support_01"],"metadata": {"ticketId": "TCK-1042"},"createdAt": "2026-07-12T10:01:00.000Z"}}}
Request Headers
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-Sendexa-Signature | hex HMAC-SHA256 of the raw body using your webhook secret |
| X-Sendexa-Event | Same as body.type (e.g. chat.message.created) |
| X-Sendexa-Delivery | Unique delivery attempt id |
| User-Agent | Sendexa-Webhooks/1.0 |
Verify Signatures
import crypto from 'crypto';import express from 'express';const app = express();// IMPORTANT: use raw body for HMAC — not a re-serialized objectapp.post('/webhooks/sendexa/chat',express.raw({ type: 'application/json' }),(req, res) => {const secret = process.env.SENDEXA_WEBHOOK_SECRET;const signature = req.get('X-Sendexa-Signature') || '';const expected = crypto.createHmac('sha256', secret).update(req.body) // Buffer.digest('hex');const valid =signature.length === expected.length &&crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));if (!valid) {return res.status(401).send('Invalid signature');}const event = JSON.parse(req.body.toString('utf8'));// Idempotency: skip if event.id already processedswitch (event.type) {case 'chat.message.created':// e.g. send FCM/APNs to offline members except authorhandleMessageCreated(event.data);break;case 'chat.member.joined':handleMemberJoined(event.data);break;case 'chat.channel.created':handleChannelCreated(event.data);break;default:// Ignore unknown types for forward compatibilitybreak;}res.status(200).json({ received: true });});
If your endpoint returns a non-2xx status or times out (10s), Sendexa retries the same event with a new X-Sendexa-Delivery id. The event id stays stable — store processed ids to ignore duplicates.
| Attempt | Delay after previous failure |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 30 minutes |
Return 200 quickly after durable enqueue (queue worker, DB write). Do heavy work asynchronously so retries are not caused by slow push providers.
Offline notifications
On chat.message.created, notify members who are not currently online via FCM / APNs / SMS.
Agent routing
On chat.channel.created, open a ticket and assign an agent; listen for chat.member.joined when they accept.
Moderation
Scan message bodies server-side and flag or soft-block abusive content without depending on the client.
Best Practices
- Deduplicate on
event.id— retries and at-least-once delivery are expected - Verify HMAC on the raw request body before JSON parsing
- Do not send push notifications to the message author; filter with
data.message.userId - Keep webhook handlers thin — enqueue work and return 200 within a few hundred milliseconds when possible
- Rotate webhook secrets periodically in the dashboard and support dual secrets during cutover if you implement it