Realtime events cookbook - Signal WS + server webhooks.
Two complementary event families. Family 1 is the Signal WebSocket (wss://<tenant>/signal.ashx) - low-latency room + call transitions consumed by browser SoftPhones and ops dashboards. Family 2 is server-side HTTP webhooks (POST to your endpoint) registered in webhooks-admin.html - durable, retried, HMAC-signed. European Digital Identity Wallet session events flow through the same webhook dispatcher.
Family 1 - Signal WebSocket
Endpoint: wss://<tenant>/signal.ashx?room=<room>&name=<peer>. No Bearer required - tenant boundary and IpAllowGate protect the socket. Server-emitted event type values verified in live code:
| Event type | Payload shape | Fire condition | Status |
|---|---|---|---|
| welcome | {type,peers[],mode,room} | Sent to a joining peer after admission. | LIVE |
| peer-joined | {type,peer:{id,name,role}} | Broadcast when a new peer enters the room. | LIVE |
| peer-left | {type,peer:{id,name}} | Broadcast when a peer disconnects. | LIVE |
| mode-changed | {type,mode:mesh|sfu} | Broadcast when the room flips mesh<->SFU. | LIVE |
| ring | {type,from,room} | Sent to a callee to trigger ringing UI. | LIVE |
| ring-cancelled | {type,reason} | Sent when the caller aborts before answer. | LIVE |
| ring-cleared | {type} | Sent when the room clears an active ring state. | LIVE |
| dial-result | {type,ok,error?} | Result of a bridge-side dial attempt. | LIVE |
| office-registered | {type,user,expires} | SoftPhone registration ack. | LIVE |
| office-call-result | {type,ok,error?} | Result of a SoftPhone outbound-call attempt. | LIVE |
| office-call-answered | {type,peer} | Callee answered a SoftPhone call. | LIVE |
| office-call-timeout | {type,reason} | Callee did not answer in time. | LIVE |
| sfu-answer | {type,sdp} | SFU SDP answer to a publisher offer. | LIVE |
| sfu-subscribe-answer | {type,sdp,participantId} | SFU SDP answer to a subscribe offer. | LIVE |
| sfu-offer-failed | {type,error} | SFU publisher offer was rejected. | LIVE |
| sfu-subscribe-failed | {type,error} | SFU subscribe offer was rejected. | LIVE |
| signal | {type,payload} | Generic relay frame (SDP/ICE). | LIVE |
| knock | {type,peer} | Broadcast to admins when a peer knocks. | LIVE |
| knock-pending | {type} | Sent to a knocker while waiting for admit. | LIVE |
| denied | {type,reason?} | Sent to a peer whose knock was denied. | LIVE |
| dup-ip-warning | {type,ip} | Broadcast when two peers share source IP. | LIVE |
| breakout-list | {type,rooms[]} | Sent when breakout rooms are (re)listed. | LIVE |
| breakout-assigned | {type,room} | Sent to a peer moved into a breakout. | LIVE |
| breakout-end | {type} | Broadcast when a breakout ends. | LIVE |
| breakout-rejected | {type,reason} | Rejection of a breakout request. | LIVE |
| pong | {type} | Server reply to a client ping. | LIVE |
| error | {type,message} | Server-side error frame. | LIVE |
Snippet matrix
| Language | Library | Recipe | Snippet |
|---|---|---|---|
| Browser JS | native WebSocket | Open | signal-ws-browser.js |
| Node.js | ws | Open | signal-ws-nodejs.js |
| Python | websockets | Open | signal-ws-python.py |
| Go | gorilla/websocket | Open | signal-ws-go.go |
| .NET | System.Net.WebSockets | Open | signal-ws-dotnet.cs |
Browser JS Recipe
native WebSocket. Download raw: signal-ws-browser.js
Node.js Recipe
Python Recipe
websockets. Download raw: signal-ws-python.py
Go Recipe
gorilla/websocket. Download raw: signal-ws-go.go
.NET Recipe
System.Net.WebSockets. Download raw: signal-ws-dotnet.cs
Family 2 - server-side webhooks
The receive side is documented in the webhooks cookbook. Below is the honest event catalog with LIVE vs RESERVED status:
| Event name | Payload shape | Status |
|---|---|---|
| call.started | call payload | LIVE |
| call.answered | call payload | LIVE |
| call.ended | call payload | LIVE |
| call.ice.connected | call payload | LIVE |
| call.transferred | call payload | LIVE |
| transcript.saved | transcript payload | LIVE |
| outbound-ai.finished | outbound payload | LIVE |
| sfu.publisher.connected | publisher payload | LIVE |
| sfu.subscriber.connected | subscriber payload | LIVE |
| room.cascade.promoted | room payload | LIVE |
| room.cascade.demoted | room payload | LIVE |
| room.cascade.failed | room payload | LIVE |
| room.peer.joined | peer payload | LIVE |
| room.peer.left | peer payload | LIVE |
| room.bandwidth.degraded | room payload | LIVE |
| sfu.offer.received | offer payload | LIVE |
| sfu.subscribe.received | subscribe payload | LIVE |
Auth model
WebSocket family. No Bearer today. The signal WebSocket is tenant-bound by request Host and gated by IpAllowGate; invite tokens (?tok=) are used for admission when a room is locked. Webhook family. Every POST carries X-CodeB-Signature: sha256=<hex>. The verify pattern is documented in the webhooks cookbook.
Reconnection + backoff
All five snippets implement the same pattern: on WS close, wait backoff, then reconnect. Start at 1 second, double on each failure, cap at 30 seconds. Reset on successful open.
RASP posture
Both surfaces enforce server-side controls: per-tenant rate limits at signal.ashx, IpAllowGate before WS accept, algorithm allow-list on HMAC signatures, tenant isolation via Host header, and loud [SIGNAL-*], [WEBHOOK-*], [api-key-diag] diagnostics at every branch. Anomalies are logged inline, not swallowed.
NIS2 / DORA / CRA
Realtime signalling and webhook streams support NIS2 incident-detection duties (near-real-time signal into a SIEM) and DORA operational-resilience monitoring. The CRA product-side auditability requirement is met by tenant-scoped, HMAC-signed webhook delivery and by loud diagnostics on every branch.
FAQ
Structured answers are embedded in the schema block above.