Appearance
Scenario: a contact center
For one business's team answering customers: a support desk, a clinic's front desk, a sales team. Customers call your WhatsApp number or your phone number; the call waits in a queue; an available agent answers in the browser (or a mobile app); supervisors watch the live board and can listen, coach or step in. For many businesses at once, read Multi-business platform after this page.
At the end you have: a line that rings your team through a queue, agents answering in your app, call history in your backend, and, if you want them, an IVR menu, overflow, supervisors, recording and reports.
Steps at a glance
| # | Step | Required? | API |
|---|---|---|---|
| 1 | Get your keys | Required | from the Calloop operator |
| 2 | Create the tenant | Required | PUT /v1/tenants/{t} |
| 3 | Add agents and supervisors | Required | PUT /v1/tenants/{t}/agents/{ref} |
| 4 | Create a queue | Required | PUT …/queues/{ref}, PUT …/queues/{ref}/members |
| 5 | Add your line and connect it | Required | PUT …/channels/{ref} (+ the WhatsApp webhook) |
| 6 | Token endpoint | Required | your backend |
| 7 | The agent app | Required | SDK |
| 8 | Receive events | Recommended | PUT /v1/webhook |
| 9 | IVR menu | Optional | POST …/audio-assets, PUT …/ivr-flows/{ref} |
| 10 | Overflow and long waits | Optional | queue fields |
| 11 | Supervisors: board and monitoring | Optional | SDK |
| 12 | Recording | Optional | line field, GET /v1/calls/{id}/recording |
| 13 | Reports and history | Optional | GET …/reports/*, GET …/calls |
| 14 | Mobile agents (push) | Optional | PUT /v1/push-credentials/*, push tokens |
| 15 | Test before launch | Required | — |
The examples use the tenant acme, the queue support and the agents emp-17 and mgr-2. Every request carries Authorization: Bearer <your API key>.
1. Get your keys
Required. Ask the Calloop operator for your consumer. You receive, once: the API key, the signing key (kid + secret) and, if they set a webhook or lookup URL for you, the webhook secret (otherwise the first PUT /v1/webhook that sets a URL returns it, once — store it), plus Calloop's base URL. For a phone number (not WhatsApp), also ask for a SIP trunk: one to your own carrier, or a shared platform trunk (the operator tells you what the carrier must support). GET /v1/sip-trunks lists the trunks you can use, with their ids.
2. Create the tenant
Required. One tenant for the business:
http
PUT /v1/tenants/acme
{ "name": "Acme Ltd",
"settings": { "team_view": true, "monitoring": { "modes": ["listen", "whisper", "barge"] } } }team_view: true: agents see the team board, the queues and their colleagues' status.monitoring.modes: what supervisors may do on calls; all three is usual for a team.- Optional:
"auto_offline": { "enabled": true, "missed_threshold": 3 }takes an agent offline after 3 missed calls in a row, so the queue stops waiting on someone who walked away.
3. Add agents and supervisors
Required. One PUT per person; the ref is your own user id.
http
PUT /v1/tenants/acme/agents/emp-17 { "name": "Rana", "role": "AGENT" }
PUT /v1/tenants/acme/agents/emp-18 { "name": "Karim", "role": "AGENT" }
PUT /v1/tenants/acme/agents/mgr-2 { "name": "Omar", "role": "SUPERVISOR" }A supervisor can also take calls. When someone leaves: DELETE /v1/tenants/acme/agents/emp-18 (they are disconnected at once).
4. Create a queue
Required. The queue is where calls wait for an available agent.
http
PUT /v1/tenants/acme/queues/support
{ "name": "Support", "strategy": "ROUND_ROBIN", "ring_timeout_seconds": 20, "max_wait_seconds": 180 }
PUT /v1/tenants/acme/queues/support/members
{ "members": [{ "agent_ref": "emp-17" }, { "agent_ref": "emp-18" }, { "agent_ref": "mgr-2" }] }| Strategy | Pick it when |
|---|---|
ROUND_ROBIN (default) | Spread calls fairly: one agent at a time, longest-idle first, moving on after ring_timeout_seconds |
RING_ALL | A small team where speed matters: everyone rings, the first to answer takes it |
PRIORITY | A main team first, others as backup: give members a priority (1 rings first) |
The members list is replaced as a whole each time. Only members who are AVAILABLE (step 7) are offered calls.
5. Add your line and connect it
Required. A WhatsApp number, a phone number, or both, each pointing at the queue.
WhatsApp (from Meta: the number's phone_number_id and a system-user access token with calling enabled):
http
PUT /v1/tenants/acme/channels/wa-main
{ "type": "WHATSAPP", "address": "+442079460000",
"provider_account_id": "<phone_number_id>",
"credentials": { "access_token": "<Meta token>" },
"inbound_queue_ref": "support" }Then connect Meta's webhook to Calloop, one of:
- Forward (recommended when your Meta app's webhook already points at your backend): in that handler, also
POSTthe same JSON body, unchanged, to/v1/webhooks/whatsapp/forwardwith your API key. Calloop keeps only the calls for your lines. - Direct: ask the Calloop operator to point the Meta app's webhook at
https://api.calloop.net/webhooks/whatsapp.
Phone number (a DID on the trunk from step 1):
http
PUT /v1/tenants/acme/channels/phone-main
{ "type": "SIP", "address": "+442079460123", "sip_trunk_id": 3, "inbound_queue_ref": "support" }Nothing more to connect: the carrier sends the number's calls to Calloop.
6. Token endpoint
Required. Your backend signs a short-lived token for the logged-in agent, with tnt: "acme", sub = their agent ref, name (always) and role AGENT or SUPERVISOR — the same role you PUT in step 3, since the token's name and role update the agent on every connect (how).
7. The agent app
Required. In the browser, with the JavaScript SDK (or the React bindings, which wrap the same calls in hooks):
js
import { connect } from '@calloop/agent-sdk';
const agent = await connect({
url: 'https://api.calloop.net',
getToken: () => fetch('/api/calloop-token').then((r) => r.text()),
});
// Queues only offer calls to available agents: your "Go available" switch.
agent.setAvailability('AVAILABLE');
agent.on('incoming', (call) => {
showRinging(call.customer); // { address, name }
call.on('remoteStream', (s) => { audioEl.srcObject = s; });
acceptBtn.onclick = () => call.accept(); // asks for the microphone
declineBtn.onclick = () => call.decline(); // passes it to the next agent
});During a call: call.hangup(), call.mute(true), and call.transfer({ agentId }) or call.transfer({ queueId }) to hand it on. agent.queues and agent.team give the waiting calls and colleagues' status for your screen. The SDK handles reconnects, page reloads and moving a call to another device.
8. Receive events
Recommended. Register your webhook (how) and store call.ended: who called, which agent answered, how long, how it ended. That is your call history and the link to your CRM. Add call.created and call.answered for live status in your own UI.
Optional: caller names. Add a lookup_url to the same PUT /v1/webhook: before ringing, Calloop asks your backend about the caller (within 1.5 s) and shows agents the name you return (lookup hook).
9. IVR menu
Optional, when callers must choose ("press 1 for sales, 2 for support"). First upload the prompt, then describe the menu. IVR nodes use Calloop's ids (from the responses), not refs. Uploading with content_base64 needs object storage on the Calloop deployment (400 otherwise); without it, register a file already in storage by its storage_key instead.
http
POST /v1/tenants/acme/audio-assets
Idempotency-Key: <uuid>
{ "name": "Main menu", "mime_type": "audio/mpeg", "content_base64": "<the MP3, base64>" }
→ 201 { "audioAsset": { "id": 12, … } }
GET /v1/tenants/acme/queues
→ { "queues": [{ "id": 3, "ref": "support", … }, { "id": 4, "ref": "sales", … }] }
PUT /v1/tenants/acme/ivr-flows/main-menu
{ "name": "Main menu", "channel_ref": "wa-main", "status": "ACTIVE", "trigger_condition": "ALWAYS",
"structure": {
"nodes": [
{ "id": "start", "type": "ivr_start", "data": {} },
{ "id": "menu", "type": "ivr_menu", "data": { "audioFileId": 12, "timeoutSeconds": 8, "noInputAction": "replay" } },
{ "id": "sales", "type": "ivr_transfer", "data": { "targetType": "queue", "targetId": 4 } },
{ "id": "support", "type": "ivr_transfer", "data": { "targetType": "queue", "targetId": 3 } }
],
"edges": [
{ "source": "start", "target": "menu" },
{ "source": "menu", "target": "sales", "sourceHandle": "1" },
{ "source": "menu", "target": "support", "sourceHandle": "2" }
] } }trigger_condition can also be ALL_AGENTS_OFFLINE (an after-hours message) or ALL_AGENTS_BUSY. Full reference: IVR flows.
10. Overflow and long waits
Optional. On the queue (step 4):
max_wait_seconds: after this long unanswered, the call moves tooverflow_queue_ref(a backup team), or ends asTIMEOUTif there is none.hold_audio_asset_id: music or a message while callers wait after an IVR.max_active_calls: a cap on calls handled at once from this queue.
11. Supervisors: board and monitoring
Optional. A user whose token has role: "SUPERVISOR" gets the live board and can monitor calls, with the same SDK (sign the role you PUT for the agent: the token's role replaces it on connect):
js
agent.on('boardCall', (call) => renderRow(call)); // every live call, as it changes
agent.on('counters', (c) => renderCounters(c)); // waiting, ringing, in progress, agents
const monitor = await agent.monitor(callId); // listen
monitor.on('stream', (s) => { supervisorAudio.srcObject = s; });
monitor.setMode('whisper'); // the agent hears you, the customer doesn't
monitor.setMode('barge'); // both hear you
monitor.stop();Only the modes in the tenant's monitoring.modes are allowed. Don't want to build these screens? Calloop's own dashboard can be opened for your staff (Dashboard launch).
12. Recording
Optional. Turn it on for your whole account (PUT /v1/recording-policy{ "enabled": true }), for the tenant (settings.recording.enabled), or per line ("recording_enabled": true in the line's PUT). After the call, the recording.completed event arrives and GET /v1/calls/{id}/recording returns a short-lived URL to a stereo Ogg file (customer left, agent right). settings.recording.retention_days on the tenant deletes old ones.
13. Reports and history
Optional.
| Need | Call |
|---|---|
| Volumes, answered/abandoned, service level, waits | GET /v1/tenants/acme/reports/calls?from=…&to=…&interval=day |
| Per agent: answered, missed, talk time | GET /v1/tenants/acme/reports/agents |
| Right now: waiting, longest wait, agents per queue | GET /v1/tenants/acme/reports/live |
| Call history with filters | GET /v1/tenants/acme/calls?agent_ref=emp-17&from=… (paged) |
| One call in full | GET /v1/calls/{id} |
14. Mobile agents (push)
Optional, only if agents use a mobile app (the Flutter SDK). Upload your push credentials once (PUT /v1/push-credentials/fcm, …/apns), and register each device: PUT /v1/tenants/acme/agents/emp-17/push-tokens/<deviceId> (integration guide → Push).
15. Test before launch
Required.
- A call to each line reaches the queue and rings an available agent; audio both ways
- An agent who is
OFFLINEis not offered calls; a declined call goes to the next agent - Nobody available: the call waits, then overflows or ends as configured
- Transfer to a colleague and to another queue
call.endedreaches your webhook, signature verified- If used: the IVR keys, a supervisor listening and whispering, a recording you can play
What happens to a queue call
| Situation | Result |
|---|---|
| An available agent answers | Talk; call.answered, then call.ended COMPLETED |
The offered agent doesn't answer in ring_timeout_seconds | Offered to the next agent (ROUND_ROBIN / PRIORITY) |
| The agent declines | Offered to the next agent; never back to that one |
| Nobody available | Waits; offered as soon as someone becomes available |
Waited max_wait_seconds | Moves to the overflow queue, or ends TIMEOUT |
| The customer hangs up while waiting | CANCELLED or NO_ANSWER |