# Human agents and queues

> MoreVoice is also a contact centre: people answer calls in the browser, from queues, with the AI in front or behind. Invite agents, build queues, send calls to them, watch the floor live, act on agents' calls and work the callbacks inbox over the API.

MoreVoice isn't only AI. It is also a cloud contact centre: your people answer and place calls in the browser, in the agent workspace, and **queues** share incoming calls between them. The AI can answer first and hand over to a queue, or take the calls nobody could answer. Over the API you invite the people, build the queues, send calls to them, watch the floor live, act on agents' calls, and work the callbacks people asked for.

| Object | ID | What it is |
| --- | --- | --- |
| User | `usr_…` | A member of your organisation who signs in to MoreVoice: an agent, a supervisor, an admin or the owner. |
| Team | `team_…` | A group of users. Queues and reports can address a whole team. |
| Queue | `q_…` | Callers waiting for a human agent, and the rules for who answers, how long callers wait and what happens then. |
| Agent status | | A user's presence right now: available, busy, in wrap-up, away or offline. |
| Callback | `cb_…` | A request to call someone back, and its progress. |

Users, teams and queues are configuration: test keys and live keys see and change the same ones (the objects' `livemode` only echoes the key). Agents answer calls in the browser, in the **Agent workspace**; the [help centre](https://help.morevoice.ai/en/agent-workspace/) shows them how.

## Add your agents

A user joins your organisation by accepting an invitation. Invite them with the role and team they should have:

**cURL**

```sh
# Invite an agent into the Support team. The invitation is emailed; it is valid for 7 days.
curl https://api.morevoice.ai/v1/invitations \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "yossi@example.com",
    "role": "agent",
    "team_id": "'"$TEAM_ID"'"
  }'
```

**Node.js**

```ts title="invite-agent.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY

// Invite an agent into the Support team. The invitation is emailed; it is valid for 7 days.
const invitation = await mv.invitations.create({ email: "yossi@example.com", role: "agent", team_id: process.env.TEAM_ID! });
console.log(invitation.id, invitation.status, invitation.expires, invitation.emailed);
```

**Python**

```python title="invite_agent.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}

# Invite an agent into the Support team. The invitation is emailed; it is valid for 7 days.
response = requests.post(
    f"{API}/invitations",
    headers=HEADERS,
    json={"email": "yossi@example.com", "role": "agent", "team_id": os.environ["TEAM_ID"]},
    timeout=30,
)
response.raise_for_status()
invitation = response.json()
print(invitation["id"], invitation["status"], invitation["expires"], invitation["emailed"])
```

- The invitation is emailed; the API never returns the link. It is valid for 7 days, and `POST /v1/invitations/{id}/resend` emails a new one.
- A seat is reserved for each open invitation: `402 plan_limit` when your plan has none free.
- API keys invite and manage **agents** and **supervisors**. Owners and admins are made by an owner, in the dashboard.
- Someone who already has a MoreVoice account (in another organisation) joins with it: `existing_user` is `true`.

Then manage people with `/v1/users` and `/v1/teams`: `PATCH /v1/users/{id}` changes a user's `role`, `team_id` or `extension`; `DELETE /v1/users/{id}` deactivates them (they are signed out, their seat is freed and their open callbacks go back to their queue), and `POST /v1/users/{id}/reactivate` brings them back. `POST /v1/teams` creates a team; deleting one leaves its members in no team.

## Build a queue

Only `name` is required; everything else takes the defaults shown on the queue object. This one shares calls between a team's agents, tells callers their place in line, offers a callback, and hands the call to the AI when nobody can take it:

**cURL**

```sh
# A support queue: the team's agents answer, callers hear their place in line, can press 1 for a callback,
# and after 3 minutes (or when no agent is signed in) the AI assistant takes the call.
curl https://api.morevoice.ai/v1/queues \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support",
    "member_team_ids": ["'"$TEAM_ID"'"],
    "strategy": "longest_idle",
    "ring_timeout_seconds": 20,
    "max_wait_seconds": 180,
    "wrap_up_seconds": 15,
    "overflow": { "when_no_agents": true, "action": "assistant", "assistant_id": "'"$ASSISTANT_ID"'" },
    "hold": { "audio": "music", "announce_position": true, "position_interval_seconds": 45 },
    "wait_keys": { "callback": "1" },
    "callbacks": { "route": "agent_queue", "priority": 6 }
  }'
```

**Node.js**

```ts title="create-queue.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY

// A support queue: the team's agents answer, callers hear their place in line, can press 1 for a callback,
// and after 3 minutes (or when no agent is signed in) the AI assistant takes the call.
const queue = await mv.queues.create({
	name: "Support",
	member_team_ids: [process.env.TEAM_ID!],
	strategy: "longest_idle",
	ring_timeout_seconds: 20,
	max_wait_seconds: 180,
	wrap_up_seconds: 15,
	overflow: { when_no_agents: true, action: "assistant", assistant_id: process.env.ASSISTANT_ID! },
	hold: { audio: "music", announce_position: true, position_interval_seconds: 45 },
	wait_keys: { callback: "1" },
	callbacks: { route: "agent_queue", priority: 6 },
});
console.log(queue.id, queue.name);
```

**Python**

```python title="create_queue.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}

# A support queue: the team's agents answer, callers hear their place in line, can press 1 for a callback,
# and after 3 minutes (or when no agent is signed in) the AI assistant takes the call.
response = requests.post(
    f"{API}/queues",
    headers=HEADERS,
    json={
        "name": "Support",
        "member_team_ids": [os.environ["TEAM_ID"]],
        "strategy": "longest_idle",
        "ring_timeout_seconds": 20,
        "max_wait_seconds": 180,
        "wrap_up_seconds": 15,
        "overflow": {"when_no_agents": True, "action": "assistant", "assistant_id": os.environ["ASSISTANT_ID"]},
        "hold": {"audio": "music", "announce_position": True, "position_interval_seconds": 45},
        "wait_keys": {"callback": "1"},
        "callbacks": {"route": "agent_queue", "priority": 6},
    },
    timeout=30,
)
response.raise_for_status()
queue = response.json()
print(queue["id"], queue["name"])
```

| Field | What it does |
| --- | --- |
| `member_user_ids`, `member_team_ids` | Who answers: users, whole teams, or both. |
| `strategy` | `longest_idle` rings the agent who has waited longest since their last call; `round_robin` takes turns. |
| `ring_timeout_seconds` | How long one agent's phone rings before the next agent is tried (5–120). |
| `max_wait_seconds` | How long a caller waits before the overflow action (0: no limit; up to an hour). |
| `wrap_up_seconds` | After-call time before an agent gets the next call (0–600). |
| `away_on_missed` | An agent who misses a call is set to away, so callers aren't offered to an empty desk. |
| `overflow` | What happens after `max_wait_seconds`, or at once when `when_no_agents` and nobody is signed in: `assistant` (the AI answers), `voicemail`, `busy`, `agent`, `queue` or `number`, with the matching `assistant_id`, `user_id`, `queue_id` or `number`. |
| `hold` | What callers hear: `music`, `ringback`, `silence`, or `asset` (a recording you uploaded, `audio_asset_id`), plus an `announcement` and their place in line every `position_interval_seconds`. |
| `wait_keys` | Keys a waiting caller presses to leave a `voicemail` or ask for a `callback` (empty: off). |
| `callbacks` | Who returns the callbacks asked for in this queue: its agents (`agent_queue`), one `agent`, or an `assistant`, and their `priority`. |
| `copilot_profile_id` | The [copilot](https://docs.morevoice.ai/guides/copilot/) profile agents get on this queue's calls. |

`PATCH /v1/queues/{id}` changes a queue: nested objects (`overflow`, `hold`, `callbacks`…) merge with the stored values, and lists replace them. The change applies to the next call at once. Deleting a queue sends its waiting callers to overflow, and inbound routes that targeted it stop doing so.

## Send calls to a queue

- **Inbound.** Give an [inbound route](https://docs.morevoice.ai/guides/inbound-calls/#targets) a `queue` target: `{ "type": "queue", "queue_id": "q_…" }`.
- **From the AI.** An assistant or a flow hands over with its transfer step, or you transfer a live AI call yourself: `POST /v1/calls/{id}/transfer` with `{ "to": { "queue_id": "q_…" }, "note": "…" }`. The agent sees your `note` next to the call. See [outbound calls](https://docs.morevoice.ai/guides/outbound-calls/#act-on-a-live-call).
- **From an agent.** An agent's call can be transferred to another queue, cold or warm: see [below](#control-a-live-call).

Your endpoints follow every queued call: [`queue.call_entered`](https://docs.morevoice.ai/webhooks/events/#queue.call_entered) (with its `position`), then [`queue.call_answered`](https://docs.morevoice.ai/webhooks/events/#queue.call_answered) (`agent_id`, `wait_ms`), [`queue.call_abandoned`](https://docs.morevoice.ai/webhooks/events/#queue.call_abandoned) when the caller hangs up first, or [`queue.overflowed`](https://docs.morevoice.ai/webhooks/events/#queue.overflowed). The call object keeps `queue_id`, and `agent_id` once an agent took it.

## Watch the floor

`GET /v1/queues/{id}/live` is the queue right now: who is waiting and for how long, the members who are signed in by status, and today's numbers, including the service level (the share of calls answered within 20 seconds):

**cURL**

```sh
# The queue right now: who waits, which agents can take them, and today's numbers.
curl https://api.morevoice.ai/v1/queues/$QUEUE_ID/live \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"
```

**Node.js**

```ts title="queue-live.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY

// The queue right now: who waits, which agents can take them, and today's numbers.
const live = await mv.queues.retrieveLive(process.env.QUEUE_ID!);
console.log(`${live.waiting} waiting, longest ${live.longest_wait_seconds ?? 0} s, ${live.agents.available} agents available`);
for (const caller of live.callers) console.log(caller.position, caller.from, `${caller.waited_seconds} s`, caller.state);
if (live.today) console.log(`today: ${live.today.answered}/${live.today.calls} answered, service level ${live.today.service_level}`);
```

**Python**

```python title="queue_live.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}

# The queue right now: who waits, which agents can take them, and today's numbers.
response = requests.get(f"{API}/queues/{os.environ['QUEUE_ID']}/live", headers=HEADERS, timeout=30)
response.raise_for_status()
live = response.json()
print(f"{live['waiting']} waiting, longest {live['longest_wait_seconds'] or 0} s, {live['agents']['available']} agents available")
for caller in live["callers"]:
    print(caller["position"], caller["from"], f"{caller['waited_seconds']} s", caller["state"])
today = live["today"]
if today:
    print(f"today: {today['answered']}/{today['calls']} answered, service level {today['service_level']}")
```

`GET /v1/agents/{id}/status` is one agent's presence, the call they are on, and the queues they answer:

**cURL**

```sh
# Your agents, then one agent's presence: available, busy on a call, in wrap-up, away or offline.
curl "https://api.morevoice.ai/v1/users?role=agent&status=active" \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"

curl https://api.morevoice.ai/v1/agents/$USER_ID/status \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"
```

**Node.js**

```ts title="agent-status.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY

// Your agents and their presence: available, busy on a call, in wrap-up, away or offline.
for await (const user of mv.users.list({ role: "agent", status: "active" })) {
	const presence = await mv.agents.retrieveStatus(user.id);
	console.log(user.name, user.extension, presence.status, presence.call?.call_id ?? "");
}
```

**Python**

```python title="agent_status.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}

# Your agents and their presence: available, busy on a call, in wrap-up, away or offline.
response = requests.get(f"{API}/users", headers=HEADERS, params={"role": "agent", "status": "active"}, timeout=30)
response.raise_for_status()
for user in response.json()["data"]:
    presence = requests.get(f"{API}/agents/{user['id']}/status", headers=HEADERS, timeout=30)
    presence.raise_for_status()
    status = presence.json()
    on_call = status["call"]["call_id"] if status["call"] else ""
    print(user["name"], user["extension"], status["status"], on_call)
```

| `status` | Meaning |
| --- | --- |
| `available` | Signed in and ready: queue calls ring them. Chosen by the agent. |
| `busy` | On a call. |
| `wrap_up` | Finishing the last call (`wrap_up_until`); no new calls until then. |
| `away` | Signed in, not taking calls: chosen by the agent, or set after a missed call. |
| `offline` | Not signed in. |

The agents set their own status in the workspace; the API reads it. [`agent.status_changed`](https://docs.morevoice.ai/webhooks/events/#agent.status_changed) and [`agent.wrapup_completed`](https://docs.morevoice.ai/webhooks/events/#agent.wrapup_completed) push the changes, so a wallboard or a workforce tool doesn't have to poll.

## Control a live call

While an agent is on a call, your code can do what the agent's call controls do: from a CRM button, a supervisor tool or an automation. Each action returns or changes the call's **control state**: who is on the call, whether the customer is on hold, and any consultation in progress.

| Action | Request | On |
| --- | --- | --- |
| Put the customer on hold, or take them off | `POST /v1/calls/{id}/hold` with `on` | Agents' calls |
| Send DTMF tones (to navigate another company's IVR) | `POST /v1/calls/{id}/dtmf` with `digits` (`,` pauses) | Agents' calls |
| Transfer to a queue, a number, an assistant or another agent, `cold` or `warm` | `POST /v1/calls/{id}/transfer` | Agents' calls; AI and IVR calls to a queue or a number |
| Finish a warm transfer: `complete`, `merge` (three-way), `swap` or `cancel` | `POST /v1/calls/{id}/consult/{action}` | Agents' calls |
| Ring someone into the call (up to 8 people), or add an assistant | `POST /v1/calls/{id}/participants` | Agents' calls |
| Mute, hold or promote a participant; remove one | `PATCH` / `DELETE /v1/calls/{id}/participants/{participant_id}` | Agents' calls |
| Read the control state | `GET /v1/calls/{id}/control` | Every call |
| End the call for everyone | `POST /v1/calls/{id}/hangup` | Every call |

A **warm transfer** puts the customer on hold while the agent talks to the target, then hands the customer over:

**cURL**

```sh
# A warm transfer on an agent's call: the agent talks to a colleague first while the customer holds…
curl https://api.morevoice.ai/v1/calls/$CALL_ID/transfer \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": { "agent_id": "'"$COLLEAGUE_ID"'" },
    "mode": "warm",
    "note": "Billing question on invoice 1042"
  }'

# …then hands the customer over (or "merge" for a three-way call, "cancel" to go back to the customer).
curl -X POST https://api.morevoice.ai/v1/calls/$CALL_ID/consult/complete \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"
```

**Node.js**

```ts title="warm-transfer.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
const callId = process.env.CALL_ID!;

// A warm transfer on an agent's call: the agent talks to a colleague first while the customer holds…
const control = await mv.calls.transfer(callId, {
	to: { agent_id: process.env.COLLEAGUE_ID! },
	mode: "warm",
	note: "Billing question on invoice 1042",
});
console.log(control.phase, control.consult?.state);

// …then hands the customer over (or "merge" for a three-way call, "cancel" to go back to the customer).
await mv.calls.consult(callId, "complete");
```

**Python**

```python title="warm_transfer.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
call_id = os.environ["CALL_ID"]

# A warm transfer on an agent's call: the agent talks to a colleague first while the customer holds…
response = requests.post(
    f"{API}/calls/{call_id}/transfer",
    headers=HEADERS,
    json={"to": {"agent_id": os.environ["COLLEAGUE_ID"]}, "mode": "warm", "note": "Billing question on invoice 1042"},
    timeout=30,
)
response.raise_for_status()
control = response.json()
print(control["phase"], control["consult"]["state"] if control["consult"] else None)

# …then hands the customer over (or "merge" for a three-way call, "cancel" to go back to the customer).
response = requests.post(f"{API}/calls/{call_id}/consult/complete", headers=HEADERS, timeout=30)
response.raise_for_status()
```

**Adding a participant** rings them in and starts a conference; the new participant is `ringing` until they answer, so follow them in the control state:

**cURL**

```sh
# Ring an expert's phone into an agent's call, then see who is on the line.
curl https://api.morevoice.ai/v1/calls/$CALL_ID/participants \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": { "number": "+97235551234" },
    "announce": "Joining you to a customer call about a claim"
  }'

curl https://api.morevoice.ai/v1/calls/$CALL_ID/control \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"
```

**Node.js**

```ts title="add-participant.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
const callId = process.env.CALL_ID!;

// Ring an expert's phone into an agent's call, then see who is on the line.
const added = await mv.calls.addParticipant(callId, {
	to: { number: "+97235551234" },
	announce: "Joining you to a customer call about a claim",
});
console.log(added.participant_id, added.state); // ringing

const control = await mv.calls.retrieveControl(callId);
for (const p of control.participants) console.log(p.participant_id, p.kind, p.name, p.state, p.muted ? "muted" : "");
```

**Python**

```python title="add_participant.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
call_id = os.environ["CALL_ID"]

# Ring an expert's phone into an agent's call, then see who is on the line.
response = requests.post(
    f"{API}/calls/{call_id}/participants",
    headers=HEADERS,
    json={"to": {"number": "+97235551234"}, "announce": "Joining you to a customer call about a claim"},
    timeout=30,
)
response.raise_for_status()
added = response.json()
print(added["participant_id"], added["state"])  # ringing

response = requests.get(f"{API}/calls/{call_id}/control", headers=HEADERS, timeout=30)
response.raise_for_status()
for p in response.json()["participants"]:
    print(p["participant_id"], p["kind"], p["name"], p["state"], "muted" if p["muted"] else "")
```

An action that doesn't fit the call answers `409`: `unsupported_for_call_mode` (hold on an AI call, say; `details.call_type` names the kind of call), `call_not_active` once the call has ended, or `transfer_failed` when the target couldn't take it (the call continues). Calls to numbers you add pass the [compliance](https://docs.morevoice.ai/guides/compliance/) gate like any outbound call.

## Callbacks

Callers who press the callback key while they wait, voicemails left in a queue (with `voicemail_to_callback`), your website's callback form, the AI and your own code all fill one **callbacks inbox**. A callback routed to a queue is offered to its next free agent, who calls back from the workspace; the inbox tracks attempts, notes and an SLA target in business hours.

**cURL**

```sh
# Callbacks waiting for the Support queue's agents, then close one that was resolved another way.
curl "https://api.morevoice.ai/v1/callbacks?status=pending&queue_id=$QUEUE_ID" \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"

curl https://api.morevoice.ai/v1/callbacks/$CALLBACK_ID/complete \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Answered by email" }'
```

**Node.js**

```ts title="callbacks.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY

// Callbacks waiting for the Support queue's agents, then close one that was resolved another way.
for await (const cb of mv.callbacks.list({ status: "pending", queue_id: process.env.QUEUE_ID! })) {
	console.log(cb.id, cb.phone, cb.name, `due ${cb.due_at}`, `priority ${cb.priority}`, cb.notes);
}
const done = await mv.callbacks.complete(process.env.CALLBACK_ID!, { note: "Answered by email" });
console.log(done.status); // completed
```

**Python**

```python title="callbacks.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}

# Callbacks waiting for the Support queue's agents, then close one that was resolved another way.
response = requests.get(
    f"{API}/callbacks",
    headers=HEADERS,
    params={"status": "pending", "queue_id": os.environ["QUEUE_ID"]},
    timeout=30,
)
response.raise_for_status()
for cb in response.json()["data"]:
    print(cb["id"], cb["phone"], cb["name"], f"due {cb['due_at']}", f"priority {cb['priority']}", cb["notes"])

response = requests.post(
    f"{API}/callbacks/{os.environ['CALLBACK_ID']}/complete",
    headers=HEADERS,
    json={"note": "Answered by email"},
    timeout=30,
)
response.raise_for_status()
print(response.json()["status"])  # completed
```

- **Create** one with `POST /v1/callbacks`: `route` is `agent_queue` (with `queue_id`), `agent` (with `agent_id`) or `assistant` (the AI calls back; see [outbound calls](https://docs.morevoice.ai/guides/outbound-calls/#call-back-later)).
- **Work it**: `PATCH` reschedules or reassigns, `POST …/notes` adds a note, `POST …/complete` closes it when it was resolved another way, `POST …/cancel` drops it.
- **Measure** with `GET /v1/callbacks/stats`: totals, how many were returned within their SLA, the average time to return, by source and by agent.
- **Events**: [`callback.scheduled`](https://docs.morevoice.ai/webhooks/events/#callback.scheduled), then `callback.completed`, `callback.failed` or `callback.cancelled`.

> **Tip:** Run your contact centre's numbers from the API too: scheduled reports (`/v1/reports`, in the [API reference](https://docs.morevoice.ai/api/)) email the dashboard's figures on a cadence, and [QA](https://docs.morevoice.ai/guides/qa/) scores every agent's calls.
