Skip to main content

Part 2D — Agents and live calls

← Contents · Previous: Part 2C — Calls · Next: Part 2E — App actions

People who answer calls. Ringmaster keeps only what it needs to ring them: a ref (the client app's user id), a display name, an optional mobile, their departments and their status. How the browser desk works end to end: Part 1 §1.7; the messages the desk receives: Part 3.

All endpoints: 🔒 Protected · Bearer API key. The agent's own buttons go through the client app's backend, which adds agentRef after checking who is logged in — the browser never holds the API key.

Set up (once)        I1 department · I7 agent · I5 members
Agent's shift        I12 lobby token → join lobby → I10 status AVAILABLE
A caller arrives     (engine rings: "ring" signal in the lobby)
                     J1 accept → join call room · or J2 decline
On the call          J3 hold · J4 resume · J5 transfer · J6 hang up
After the call       WRAP_UP for wrapUpSec → AVAILABLE (or I11 ready)
End of shift         I10 status OFFLINE → leave lobby

I. Departments and agents

I1. Create a department

Endpoint: POST {base_url}/accounts/{accountId}/departments

Request JSON Sample:

{
  "name": "Mapokezi",
  "strategy": "RING_ALL",
  "ringTimeoutSec": 20,
  "wrapUpSec": 30,
  "maxMissedBeforeAway": 5,
  "ifNobodyOnline": "WAIT",
  "record": false
}
Parameter Type Required Description Validation / default
name string Yes Shown to callers' agents and in reports not blank
strategy string No RING_ALL (every free member at once) · ROUND_ROBIN (one at a time, in member order, starting after the last one rung) RING_ALL
ringTimeoutSec integer No How long one ring lasts before the next try 10–60, default 20
wrapUpSec integer No Quiet time after each call before the agent is rung again 0–300, default 0
maxMissedBeforeAway integer No Missed or declined rings in a row before the agent is set AWAY; 0 = never 0–20, default 5
ifNobodyOnline string No WAIT (caller waits in the waiting room) · EXIT_NO_ANSWER (the Connect block's noAnswer exit at once) WAIT
record boolean No Record talk in this department (when the account's recording mode allows) true

Success Response JSON Sample (201 Created, message "Department created"):

{
  "success": true,
  "httpStatus": "201 CREATED",
  "message": "Department created",
  "action": null,
  "action_time": "2026-11-13T20:10:47.69580505",
  "data": {
    "id": "c09c1657-d18a-4a32-94c7-4ffa24e038c3",
    "name": "Mapokezi",
    "strategy": "RING_ALL",
    "ringTimeoutSec": 20,
    "wrapUpSec": 0,
    "maxMissedBeforeAway": 5,
    "ifNobodyOnline": "WAIT",
    "record": false,
    "members": [],
    "waiting": 0
  }
}
Field Description
id Put it in a flow's Connect block: "department": "<id>"
members Agent refs in ring order
waiting Callers waiting now

Errors:

{
  "success": false,
  "httpStatus": "422 UNPROCESSABLE_CONTENT",
  "message": "Validation failed",
  "code": "VALIDATION_FAILED",
  "action_time": "2026-11-13T20:10:48.000000000",
  "data": { "ringTimeoutSec": "must be greater than or equal to 10" }
}

I2. List departments

Endpoint: GET {base_url}/accounts/{accountId}/departments

Success Response: data = array of departments (I1 shape), by name.

I3. Update a department

Endpoint: PUT {base_url}/accounts/{accountId}/departments/{departmentId}

Request: same as I1; name is required, missing settings keep their value. Success Response: the department, message "Department updated". Errors: 404 "Department not found".

I4. Delete a department

Endpoint: DELETE {base_url}/accounts/{accountId}/departments/{departmentId}

Success Response: 200, message "Department deleted", data null. Its members are removed from it.

Errors:

{
  "success": false,
  "httpStatus": "409 CONFLICT",
  "message": "Department is used by live script versions",
  "code": "DEPARTMENT_IN_USE",
  "action_time": "2026-11-13T20:11:00.000000000",
  "data": { "usedBy": ["recorded-music v2"] }
}

409 DEPARTMENT_BUSY "Callers are still waiting in this department" — try again when the queue is empty.


I5. Set a department's members

Purpose: Replace the whole member list; the order is the round-robin order. Callers already waiting are offered to new free members at once.

Endpoint: PUT {base_url}/accounts/{accountId}/departments/{departmentId}/members

Request JSON Sample:

{ "agentRefs": ["asha", "juma", "neema"] }
Parameter Type Required Description
agentRefs string[] Yes Existing agents of the account; [] empties the department; repeats are ignored

Success Response: the department, message "Members set".

Errors: 404 "Agent not found: ghost" — nothing is changed.


I6. Callers waiting in a department

Endpoint: GET {base_url}/accounts/{accountId}/departments/{departmentId}/queue

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Waiting callers",
  "action": null,
  "action_time": "2026-11-13T20:10:47.790000000",
  "data": [
    { "callId": "8a5e282d-93a1-443b-81f3-4eb7294a0639", "from": "+255745051250", "position": 1, "waitingSeconds": 27 }
  ]
}

Order: callers put back at the head (a failed transfer, an agent who dropped) first, then by waiting time.


I7. Add or update an agent

Endpoint: PUT {base_url}/accounts/{accountId}/agents/{ref}

Request JSON Sample:

{ "displayName": "Asha (mapokezi)", "mobile": "+255712345678" }
Parameter Type Required Description Validation
{ref} (path) string Yes The client app's user id
displayName string Yes Shown in the desk, wallboard and reports not blank
mobile string No Rung when the number's ring mode includes MOBILE; "" removes it; missing keeps it a phone number

Success Response JSON Sample (message "Agent saved"; a new agent starts OFFLINE):

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Agent saved",
  "action": null,
  "action_time": "2026-11-13T20:10:47.726488473",
  "data": {
    "ref": "asha",
    "displayName": "Asha (mapokezi)",
    "mobile": null,
    "status": "AVAILABLE",
    "online": true,
    "missedInRow": 0,
    "currentCallId": null,
    "departments": ["c09c1657-d18a-4a32-94c7-4ffa24e038c3"]
  }
}
Field Description
status OFFLINE · AVAILABLE · RINGING · BUSY · WRAP_UP · AWAY (Part 1 §1.7)
online The agent's browser is connected to the lobby now
missedInRow Missed or declined rings since the last answered one
currentCallId The call ringing or live with them
departments Department ids they are a member of

Errors: 422 {"displayName": "must not be blank"} · 400 "mobile: …" (not a phone number).


I8. List agents

Endpoint: GET {base_url}/accounts/{accountId}/agents

Success Response: data = array of agents (I7 shape), by display name. Use it for a wallboard together with agent.status webhooks.

I9. Remove an agent

Endpoint: DELETE {base_url}/accounts/{accountId}/agents/{ref}

Success Response: 200, message "Agent removed". They leave every department and are never rung again; a call in progress with them finishes normally. Errors: 404 "Agent not found: …".


I10. Set an agent's status

Purpose: The agent's own buttons: Available, Away (break), Offline (end of shift). RINGING, BUSY and WRAP_UP are set by calls, never by hand.

Endpoint: PUT {base_url}/accounts/{accountId}/agents/{ref}/status

Request JSON Sample:

{ "status": "AVAILABLE" }
status Effect
AVAILABLE Can be rung; missedInRow back to 0; a caller already waiting is offered at once
AWAY Online but not rung (a ring in progress is cancelled)
OFFLINE Not rung

Success Response: the agent, message "Status set". An agent.status webhook follows.

Errors:

  • 400 "Status can be set to AVAILABLE, AWAY or OFFLINE"
  • 409 AGENT_BUSY "Finish the current call first"

Being online (connected to the lobby) is separate from the status: an agent whose browser is gone for more than 30 s (and who is not on a call) is set OFFLINE by the engine.


I11. Ready (end wrap-up early)

Endpoint: POST {base_url}/accounts/{accountId}/agents/{ref}/ready

Success Response: the agent (AVAILABLE if they were in WRAP_UP), message "Ready".


I12. Lobby token

Purpose: Lets the agent's browser join their lobby room, where the engine sends ring signals. Data only: no microphone. Valid for 10 minutes; ask for a new one before it runs out (an open connection stays up).

Endpoint: POST {base_url}/accounts/{accountId}/agents/{ref}/lobby-token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Lobby token",
  "action": null,
  "action_time": "2026-11-13T20:10:47.815393198",
  "data": {
    "url": "wss://<livekit-host>",
    "room": "lobby-0ea57aeb-c3c9-48f5-b366-7fe3e457ffbd",
    "identity": "agent-0ea57aeb-c3c9-48f5-b366-7fe3e457ffbd",
    "token": "eyJhbGciOiJIUzI1NiIsInR5…"
  }
}

In the browser (LiveKit JS SDK):

const room = new LivekitClient.Room();
room.on(LivekitClient.RoomEvent.DataReceived, bytes => onSignal(JSON.parse(new TextDecoder().decode(bytes))));
await room.connect(data.url, data.token);           // then PUT …/status {AVAILABLE} through the backend

Make the browser's ring sound start from the agent's "Start shift" click: browsers mute sound that did not start from a click.


I13. Agent stats

Endpoint: GET {base_url}/accounts/{accountId}/agents/stats

Query Type Required Description Default
from / to date No YYYY-MM-DD, account time the last 7 days

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Agent stats",
  "action": null,
  "action_time": "2026-11-13T20:10:47.759658701",
  "data": [
    { "key": "asha", "values": { "offered": 7, "answered": 3, "missed": 2, "declined": 0, "talkSeconds": 50.0 } }
  ]
}
values Description
offered Rings
answered Accepted
missed Rang until the ring time ran out
declined Pressed Decline
talkSeconds Time talking to callers

J. Live call controls

Every control takes the agent: {"agentRef": "asha"} (sent by the client app's backend). Controls for a call that is not ringing for, or not live with, that agent are refused.

J1. Accept a ringing call

Endpoint: POST {base_url}/calls/{callId}/accept

Request JSON Sample:

{ "agentRef": "asha" }

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Call accepted",
  "action": null,
  "action_time": "2026-11-13T19:43:46.998000000",
  "data": {
    "url": "wss://<livekit-host>",
    "room": "call-_+255745051250_uqL6Uaw9VmC9",
    "identity": "agent-0ea57aeb-c3c9-48f5-b366-7fe3e457ffbd",
    "token": "eyJhbGciOiJIUzI1NiIsInR5…"
  }
}

The browser joins room with token and turns the microphone on; the caller's waiting music stops and they are connected as soon as the agent is in the room (the engine waits up to 15 s; if the agent never arrives, the caller goes back to the head of the queue). Other agents rung for the same call get ring_cancel (answered_elsewhere).

Errors:

  • 409 NOT_RINGING "This call is not ringing for you any more" (ring time ran out, the caller left, or it was cancelled)
  • 409 ALREADY_TAKEN "Another agent already answered this call"
  • 404 "Call not found" · "Agent not found: …"

J2. Decline a ringing call

Endpoint: POST {base_url}/calls/{callId}/decline

Request: {"agentRef": "asha"}. Success Response: 200, message "Call declined". The engine rings the next agent; the decline counts as a missed ring (I1 maxMissedBeforeAway). Errors: 409 NOT_RINGING.


J3. Hold

Purpose: The caller hears hold music; the agent's microphone is muted for the caller.

Endpoint: POST {base_url}/calls/{callId}/hold

Request: {"agentRef": "asha"}. Success Response: 200, message "On hold". Holding twice is harmless.

Errors:

{
  "success": false,
  "httpStatus": "409 CONFLICT",
  "message": "This call is not with you",
  "code": "NOT_YOUR_CALL",
  "action_time": "2026-11-13T20:10:57.000000000",
  "data": null
}

J4. Resume from hold

Endpoint: POST {base_url}/calls/{callId}/unhold

Request: {"agentRef": "asha"}. Success Response: 200, message "Back from hold". Errors: 409 NOT_YOUR_CALL.


J5. Transfer

Purpose: Hand the caller to another department or a named agent (blind transfer: the first agent leaves at once, the caller hears music meanwhile).

Endpoint: POST {base_url}/calls/{callId}/transfer

Request JSON Samples:

{ "agentRef": "asha", "toDepartmentId": "dbd644af-e460-40fa-9b09-4cb838915bdc" }
{ "agentRef": "asha", "toAgentRef": "juma" }
Parameter Type Required Description
agentRef string Yes The agent now on the call
toDepartmentId uuid One of the two The caller goes to the head of that department's queue
toAgentRef string One of the two That agent is rung; if they don't answer, the caller's current department picks them up (they are at its head)

Success Response: 200, message "Transferring". The call's timeline gets a TRANSFER event ({from, to}).

Errors: 400 "Give toDepartmentId or toAgentRef" · 409 NOT_YOUR_CALL · 404 "Agent not found: …".


J6. Hang up

Endpoint: POST {base_url}/calls/{callId}/hangup

Request: {"agentRef": "asha"}. Success Response: 200, message "Hanging up". The caller is disconnected; the call ends with result: HANDLED_BY_AGENT, endReason: AGENT_HUNG_UP, endedBy: AGENT; the agent goes to WRAP_UP (or straight to AVAILABLE when wrapUpSec is 0).

Errors: 409 NOT_YOUR_CALL.

If the agent's browser drops during a call (closed tab, lost network), the caller is not cut off: they hear music and go back to the head of the queue.