Calls & rooms

Every conversation can hold one live call at a time. It is voice or video, for up to 16 people. The audio and video go through LiveKit; you don't set up or configure it. Ringing, presence, the roster and call history are handled by poolse itself.

Calls and rooms

There are two kinds. Both have kind in every payload.

callroom
Started withPOST /v1/conversations/:id/callsPOST /v1/conversations/:id/room
Rings peopleYes. Every other member's phones ring.No. Members see a "Join" banner.
EndsWhen at most one person is left and nobody is still ringing.When it has been empty for a short while.
Typical usePhone-style calling (WhatsApp, FaceTime)Drop-in voice or video next to the chat (Meet, Slack huddles)
SDK@poolse/native place() on phones; poolse.rooms.ring() on the web@poolse/native startRoom(); poolse.rooms.start() / useRoom on the web

Both are joined the same way, POST /v1/calls/:id/join, by any member at any time while they are live. Both show up in the same events (call:started, call:participants, call:finished), the same history, and the same webhooks.

Starting either one while something is already live on the conversation joins what is live instead. The response is 200 instead of 201, with created: false. If two members call each other at the same moment, they end up in one call.

The endpoints are on Calls.

The lifecycle

  1. Placed. The caller is in the call from the start. poolse rings every other member's devices by push, creates the call record (status: "live"), sends call:started to the conversation channel, and fires the call.started webhook.
  2. Ringing. Each person rung can answer, decline, or let it ring out. Rings stop at the workspace's ring time (default 45 seconds).
  3. Answered. The first answer fires call.answered. The call stays joinable for everyone else who was rung, and for any member from the banner. Every join and leave sends call:participants.
  4. Ended. See below. poolse sends call:finished, stops any phone still ringing, closes the media room, writes the call log, sends missed-call notifications, completes the record and fires call.finished.

A room skips the ringing. It goes live when it is started, and people come and go.

Ringing and devices

  • Every registered device of each person rung gets a ring push: the PushKit token on iPhones, the FCM token on Android. A person with no registered device is not rung; the record shows them as missed.
  • The ring push carries a ring grant, a token bound to that call, person and device. It lets a locked phone with no session answer or decline (POST /v1/calls/:id/answer or /decline).
  • Answering on one device stops the ring on that person's other devices (ring_ended with answered_elsewhere).
  • Declining ends that one person's ring and stops their other devices. It never ends the call for anyone else. A person who declined can still join from the banner while the others talk.
  • Busy. A phone already on a call can decline with reason: "busy". @poolse/native does this when a second call arrives and the user turns it down (call waiting). It is recorded as busy, not declined.
  • Add people. Once someone has answered, anyone in the call can ring more of the conversation's members into it (POST /v1/calls/:id/ring). They are rung exactly like the first time, with their own ring window.

How a call ends

reasonWhen
finishedA call is down to at most one person with nobody still ringing. If someone hangs up and leaves one person alone, it ends at once. If the media server reports people dropping off, it ends after 30 seconds, giving them time to reconnect. A room ends 3 seconds after its last person leaves, or 30 seconds after it opened if nobody ever connected.
cancelledThe caller hung up while it was still ringing and nobody had answered.
no_answerThe ring time ran out and nobody had answered.
declined_allEveryone rung declined before anyone answered.
busy_allEveryone rung was on another call.

Leaving a room never ends it for the others.

Each person's outcome (status) is one of:

  • joined: was in the call at some point. This beats a decline on another device.
  • declined: declined the ring.
  • busy: their phone refused the ring because they were on another call.
  • missed: was rung and never answered.
  • invited: still ringing. Seen in history only while a call is live.

The caller is never in the participants list. They are started_by.

Reconnecting

If a phone loses its connection mid-call, it asks for a fresh session with POST /v1/calls/:id/reconnect, using the call's control token. @poolse/native does this for you. A call that ended in the meantime answers 410 call_finished.

Limits

Set per workspace on the dashboard's Calls page. poolse enforces them on the server for every client.

SettingRangeDefaultEffect
People per call2–1616A join or answer that would go over answers 409 call_full. Someone already in the call (a reconnect, a second device) always gets back in.
Videoon / offonOff makes every call and room audio-only, and the media token cannot publish a camera.
Ring time15–120 s45 sHow long phones ring before the call is missed.

Every call summary carries video_allowed and max_participants, so your app can hide controls that would not work.

The call log

When a call or room ends, poolse posts one system message in the conversation. It arrives like any message (message:new, message.created) and counts toward unread. Its body is a JSON string:

{
  "kind": "call",
  "v": 2,
  "call_kind": "call",
  "call_id": "4a69…",
  "media": "video",
  "caller_user_id": "2b7e…",
  "started_at": 1793952000000,
  "ended_at": 1793952265000,
  "end_reason": "finished",
  "duration_ms": 260000,
  "joined_user_ids": ["2b7e…", "9c41…"],
  "participants": [
    { "user_id": "9c41…", "status": "joined" },
    { "user_id": "a7d2…", "status": "missed" }
  ]
}
  • started_at and ended_at are epoch milliseconds here.
  • duration_ms is null when nobody answered.
  • participants excludes the caller.

Word it for each viewer: "Outgoing call · 4:20" for the caller, "Missed call" for someone whose status is missed. describeCallLog, callLogDisplayFor and parseCallLog in @poolse/sdk do this. <CallLogRow> in the UI kits renders it. A call log message does not send a chat notification. Missed calls have their own push.

Call history

Every call and room has a record from the moment it starts. Your backend lists them with GET /v1/calls, an app with GET /v1/me/calls or GET /v1/conversations/:id/calls. See Call history and Building a calls tab.

Minutes

poolse measures calling in participant-minutes, audio and video apart. Each person's time counts from when they enter the call to when they leave it or it ends. For a call's caller, that starts once someone else is in the call, so ringing costs nothing and an unanswered call leaves no minutes. In a room, time counts from arrival. A session is counted as video if the call was a video call when the person joined; a voice call or room becomes video for everyone in it the moment anyone turns a camera on. Totals per calendar month (UTC) are rounded up to whole minutes.

Usage is shown on the dashboard's Calls and Billing pages. There is no API for it.

Each plan includes a monthly allowance per medium (Free 100 voice / 50 video, Pro 10,000 / 2,000, Scale 100,000 / 25,000). Once one is used up, new calls and rooms of that medium are refused with 402 call_minutes_exhausted until the month turns; calls already connected carry on. See Limits & quotas.

Not available

  • Recording, transcription, or live streaming.
  • Screen sharing through the SDK.
  • Calling a phone number (PSTN) or SIP.
  • More than one live call per conversation, or more than 16 people.
  • Calling someone outside the conversation. Add them as a member first.
  • Usage or reliability figures through the API. They are on the dashboard only.
Calls & rooms · Concepts · poolse docs