# Part 2D — Agents and live calls

[← Contents](README.md) · Previous: [Part 2C — Calls](2c-calls.md) · Next: [Part 2E — App actions](2e-actions.md)

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](1-how-it-works.md#17-agents-in-the-browser); the messages the desk receives: [Part 3](3-webhooks.md#33-agent-desk-signals).

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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/accounts/{accountId}/departments`

**Request JSON Sample**:
```json
{
  "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"):
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/accounts/{accountId}/departments`

**Success Response**: `data` = array of departments ([I1](#i1-create-a-department) shape), by name.

## I3. Update a department
**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/accounts/{accountId}/departments/{departmentId}`

**Request**: same as [I1](#i1-create-a-department); `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**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/accounts/{accountId}/departments/{departmentId}`

**Success Response**: `200`, `message` "Department deleted", `data` null. Its members are removed from it.

**Errors**:
```json
{
  "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**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/accounts/{accountId}/departments/{departmentId}/members`

**Request JSON Sample**:
```json
{ "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/accounts/{accountId}/departments/{departmentId}/queue`

**Success Response JSON Sample**:
```json
{
  "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**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/accounts/{accountId}/agents/{ref}`

**Request JSON Sample**:
```json
{ "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`):
```json
{
  "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](1-how-it-works.md#17-agents-in-the-browser)) |
| `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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/accounts/{accountId}/agents`

**Success Response**: `data` = array of agents ([I7](#i7-add-or-update-an-agent) shape), by display name. Use it for a
wallboard together with `agent.status` webhooks.

## I9. Remove an agent
**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{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**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/accounts/{accountId}/agents/{ref}/status`

**Request JSON Sample**:
```json
{ "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/accounts/{accountId}/agents/{ref}/lobby-token`

**Success Response JSON Sample**:
```json
{
  "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):
```js
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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/calls/{callId}/accept`

**Request JSON Sample**:
```json
{ "agentRef": "asha" }
```

**Success Response JSON Sample**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{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](#i1-create-a-department) `maxMissedBeforeAway`).
**Errors**: `409 NOT_RINGING`.

---

## J3. Hold
**Purpose**: The caller hears hold music; the agent's microphone is muted for the caller.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/calls/{callId}/hold`

**Request**: `{"agentRef": "asha"}`. **Success Response**: `200`, `message` "On hold". Holding twice is harmless.

**Errors**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/calls/{callId}/transfer`

**Request JSON Samples**:
```json
{ "agentRef": "asha", "toDepartmentId": "dbd644af-e460-40fa-9b09-4cb838915bdc" }
```
```json
{ "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{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.