Webhooks
Subscribe to interactive room lifecycle, live stream status (started, live, disconnected, ended), participant presence, and recording events. Sendexa POSTs signed JSON so you can update UIs, attendance, and VOD archives without polling.
Real-time lifecycle
Know when rooms and live streams go active or end without polling REST.
Presence events
Track who joined and left interactive rooms with identity and role.
Broadcast events
stream.started, stream.live, stream.disconnected, stream.ended.
Recording ready
Signed download URL when room or stream VOD finishes processing.
<500ms
event → first POST (p95)
5s
return 2xx quickly
6
exponential backoff ~24h
9
room · stream · recording
Configuration
video.* event family. See the platform webhooks overview for shared delivery semantics.Always verify signatures
X-Sendexa-Signature (or X-Signature) using your webhook secret. Never process unsigned bodies.Event catalog
First participant connected; room status is now active.
Room ended via API, empty timeout, or forced disconnect.
A client completed media handshake and entered the room.
A participant disconnected (client leave, network, or room end).
Encoder connected; stream status is starting (ladder building).
Playback manifests are ready; viewers can join HLS / LL-HLS.
Encoder dropped; reconnect window is open.
Stream closed permanently (API end or reconnect timeout).
Room composite or stream VOD is available with a signed URL.
Payload envelope
All video webhooks share a common top-level shape. Event-specific fields live under data.
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | Unique delivery / event id (use for idempotency). |
| type | string | required | Event name, e.g. video.participant.joined |
| createdAt | ISO 8601 | required | When the event occurred on Sendexa. |
| businessId | string | required | Your business / account id. |
| data | object | required | Event payload (room, participant, or recording fields). |
{"id": "evt_vid_01J0ABCXYZ","type": "video.participant.joined","createdAt": "2026-07-12T10:16:45.120Z","businessId": "BU-VOIF86X7","apiVersion": "v1","data": { }}
Fired once when the room transitions from ready → active.
{"id": "evt_vid_01J0START","type": "video.room.started","createdAt": "2026-07-12T10:16:02.000Z","businessId": "BU-VOIF86X7","apiVersion": "v1","data": {"roomId": "rm_8f3a2c1b0e9d","name": "Support Session #1042","status": "active","region": "af-west","record": true,"maxParticipants": 4,"participantCount": 1,"startedAt": "2026-07-12T10:16:02.000Z","metadata": { "ticketId": "TKT-1042" },"firstParticipant": {"identity": "agent-ada","role": "host"}}}
HTTP headers
| Header | Example | Description |
|---|---|---|
| Content-Type | application/json | JSON body |
| X-Sendexa-Signature | sha256=abc123... | HMAC-SHA256 of raw body with webhook secret |
| X-Sendexa-Timestamp | 2026-07-12T10:16:45.120Z | Send time — reject if too old (replay protection) |
| X-Webhook-ID | whk_vid_991 | Delivery id (distinct from event id on retries) |
| X-Attempt | 1 | Delivery attempt number (1–6) |
Verify signatures
import crypto from 'crypto';import express from 'express';const app = express();// Important: verify against the raw bodyapp.post('/webhooks/sendexa-video',express.raw({ type: 'application/json' }),(req, res) => {const secret = process.env.SENDEXA_WEBHOOK_SECRET;const signature = req.get('X-Sendexa-Signature') || '';const raw = req.body; // Bufferconst expected ='sha256=' +crypto.createHmac('sha256', secret).update(raw).digest('hex');const ok =signature.length === expected.length &&crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));if (!ok) {return res.status(401).send('invalid signature');}const event = JSON.parse(raw.toString('utf8'));// Idempotency: skip if event.id already processedswitch (event.type) {case 'video.room.started':// mark session livebreak;case 'video.room.ended':// close ticket timerbreak;case 'video.participant.joined':case 'video.participant.left':// attendancebreak;case 'video.recording.ready':// enqueue download of event.data.urlbreak;default:break;}// Respond fast — heavy work goes to a queueres.status(200).json({ received: true });});
Non-2xx responses or timeouts (>5s) trigger retries with exponential backoff. Deliveries are at-least-once — design handlers to be idempotent using id / X-Webhook-ID.
| Attempt | Delay | Description |
|---|---|---|
| 1 | 5 seconds | Immediate follow-up |
| 2 | 30 seconds | Second attempt |
| 3 | 5 minutes | Third attempt |
| 4 | 30 minutes | Fourth attempt |
| 5 | 2 hours | Fifth attempt |
| 6 | 6 hours | Final attempt within 24h window |
Respond quickly
Return 2xx within 5 seconds. Push downloads and CRM updates to a queue.
Idempotent processing
Store processed event ids. Retries and network duplicates are expected.
Archive recordings
On recording.ready, fetch the signed URL immediately and store in your bucket.
Join metadata
Correlate room.metadata.ticketId (or similar) to update the right domain object.
Common pitfalls
- Verifying a re-serialized JSON body instead of the raw request bytes
- Blocking the webhook request while downloading a large recording
- Assuming exactly-once delivery — always dedupe
- Ignoring
video.room.endedand leaving CRM sessions open indefinitely
Related docs
- Rooms API — create / end sessions that emit these events
- Tokens — identities that appear on participant events
- Platform webhooks — shared security and logging