Human agents and queues
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 shows them how.
Add your agents
Section titled “Add your agents”A user joins your organisation by accepting an invitation. Invite them with the role and team they should have:
# 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"'" }'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);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}/resendemails a new one. - A seat is reserved for each open invitation:
402 plan_limitwhen 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_useristrue.
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
Section titled “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:
# 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 } }'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);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 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
Section titled “Send calls to a queue”- Inbound. Give an inbound route a
queuetarget:{ "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}/transferwith{ "to": { "queue_id": "q_…" }, "note": "…" }. The agent sees yournotenext to the call. See outbound calls. - From an agent. An agent’s call can be transferred to another queue, cold or warm: see below.
Your endpoints follow every queued call: queue.call_entered (with its position), then queue.call_answered (agent_id, wait_ms), queue.call_abandoned when the caller hangs up first, or queue.overflowed. The call object keeps queue_id, and agent_id once an agent took it.
Watch the floor
Section titled “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):
# 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"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}`);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:
# 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"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 ?? "");}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 and agent.wrapup_completed push the changes, so a wallboard or a workforce tool doesn’t have to poll.
Control a live call
Section titled “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:
# 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"import MoreVoice from "@morevoice/sdk";
const mv = new MoreVoice(); // reads MOREVOICE_API_KEYconst 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");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:
# 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"import MoreVoice from "@morevoice/sdk";
const mv = new MoreVoice(); // reads MOREVOICE_API_KEYconst 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" : "");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 gate like any outbound call.
Callbacks
Section titled “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.
# 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" }'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); // completedimport 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:routeisagent_queue(withqueue_id),agent(withagent_id) orassistant(the AI calls back; see outbound calls). - Work it:
PATCHreschedules or reassigns,POST …/notesadds a note,POST …/completecloses it when it was resolved another way,POST …/canceldrops 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, thencallback.completed,callback.failedorcallback.cancelled.