Skip to main content

Events WebSocket

GET /api/v1/events upgrades to a WebSocket that streams your company's live events as JSON text frames. This is how an integration follows calls and configuration changes. MonoPbx does not call your server: there are no outbound webhooks yet.

Event payloads are JSON envelopes; their JSON Schema is served at GET /api/v1/events/schema.

Connecting​

Authenticate first, then subscribe. Nothing is subscribed, replayed or sent before authentication succeeds.

ClientHow to authenticate
Server, CLI, integrationSend Authorization: Bearer <access token or mpbx_k1_ API key> on the upgrade request. A bad credential is a plain 401 and no socket.
Browser (a browser WebSocket cannot set headers)Connect with no credential and send {"op":"auth","token":"..."} as the first frame within 5 seconds. Any other first frame, a wrong token or silence closes the socket with code 4401.

Never put the token in the query string. URLs end up in logs. Cookies are never accepted on this route either, so a page on another site cannot open the socket with your session.

A system principal selects the company with X-Mpbx-Tenant. A company credential cannot select another company (403).

// Browser
const ws = new WebSocket("wss://pbx.example.com/api/v1/events");
ws.onopen = () => ws.send(JSON.stringify({ op: "auth", token: accessToken }));
ws.onmessage = (m) => {
const f = JSON.parse(m.data);
if (f.op === "ready") ws.send(JSON.stringify({ op: "subscribe", topics: ["call", "presence"] }));
if (f.op === "event") handle(f.event, f.cursor);
};

Optionally pass topics and cursor in the query (?topics=call,presence&cursor=<event id>) to subscribe right after ready. Without them the socket waits for a subscribe frame.

Frames​

Client to server:

FrameMeaning
{"op":"auth","token":"..."}First frame of a socket that sent no header credential.
{"op":"subscribe","topics":["call","presence"],"cursor":"<event id>"}Add topics. Omitting topics (or an empty list) means every topic you may see. cursor resumes after that event and is accepted only in the first subscribe.
{"op":"unsubscribe","topics":["presence"]}Remove topics.
{"op":"ping"}Answered with {"op":"pong"}.

Server to client:

FrameMeaning
{"op":"ready","protocol":1,"tenant_id":"...","topics":[...],"heartbeat_secs":20}Authenticated. topics are the topics you may subscribe to.
{"op":"subscribed","topics":[...],"denied":[...],"resync_required":false}The active topics. denied lists requested topics you may not see.
{"op":"event","cursor":"<event id>","event":{...}}One event. cursor equals event.id.
{"op":"replay_done","count":3}The events after your cursor were sent; live events follow.
{"op":"heartbeat","ts":1790000000000}Liveness, every heartbeat_secs.
{"op":"resync_required","reason":"events_lost"}The server lost events. Reload your state over REST.
{"op":"error","code":"MPBX-EVENTS-WS-...","detail":"..."}A refused frame. The socket stays open, except after repeated malformed frames (close 1008).

Delivery is at-least-once: de-duplicate on the event id. Events from one producer arrive in order.

Topics and permissions​

You only receive your own company's events, and only those your role allows:

TopicPermission neededScope
call, legcalls:controldepartment
queuequeues:readdepartment
presencedirectory:readcompany-wide
voicemailvoicemail:readdepartment
configthe read permission of the changed entity (for example users:read for a user)
auditaudit:readcompany-wide
devicedevices:readdepartment

A signed-in user always sees the events they take part in (their own calls, presence and mailbox). An API key does not get this: it sees only what its scopes allow. A role change takes effect within 60 seconds, and an expired or revoked credential closes the socket with 4401 in the same time.

Call, leg, queue, presence and voicemail events are published as those features land, so some topics may carry no events yet.

Resuming after a disconnect​

Keep the cursor of the last event you processed. After a reconnect, open the socket with ?cursor=<it> (or put it in the first subscribe). The server sends your company's events after it, then replay_done, then live events.

The server keeps the last 8192 events in memory. If your cursor is outside that window, the server restarted, or the id is not one of your company's events, subscribed.resync_required is true and nothing is replayed: reload your state over REST, then continue from the live stream. Durable replay across restarts is not built yet.

Limits​

SettingDefault
Heartbeatevery 20 s
Idle timeout (no frame from you; a pong counts)60 s, then close 4408
Time to send the auth frame5 s
Frames queued per connection256; a queue that stays full for 5 s closes the socket (1013)
Largest client frame16 KiB
Open connections10 000 per install, 5 000 per company, 20 per user or key

A client that reads too slowly is disconnected and never slows others down; reconnect with your cursor.

Close codes: 4401 not authenticated or credential ended, 4403 no company selected, 4408 idle, 4429 too many connections (or closed to make room for another company), 1008 protocol abuse, 1011 server error, 1012 server restarting, 1013 slow consumer. For 1011, 1012 and 1013, reconnect with your cursor.

Click-to-call​

POST /api/v1/calls/click-to-call with {"destination":"+15550123","device_id":"<uuid>"} is a published contract that currently answers 501 (MPBX-NOT-IMPLEMENTED). It authenticates the caller and validates the body (400 for a bad destination), then stops. Originating a call from the API is still to come; the request and response will not change when it does.