Skip to content

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 ​

#StepRequired?API
1Get your keysRequiredfrom the Calloop operator
2Create the tenantRequiredPUT /v1/tenants/{t}
3Add agents and supervisorsRequiredPUT /v1/tenants/{t}/agents/{ref}
4Create a queueRequiredPUT …/queues/{ref}, PUT …/queues/{ref}/members
5Add your line and connect itRequiredPUT …/channels/{ref} (+ the WhatsApp webhook)
6Token endpointRequiredyour backend
7The agent appRequiredSDK
8Receive eventsRecommendedPUT /v1/webhook
9IVR menuOptionalPOST …/audio-assets, PUT …/ivr-flows/{ref}
10Overflow and long waitsOptionalqueue fields
11Supervisors: board and monitoringOptionalSDK
12RecordingOptionalline field, GET /v1/calls/{id}/recording
13Reports and historyOptionalGET …/reports/*, GET …/calls
14Mobile agents (push)OptionalPUT /v1/push-credentials/*, push tokens
15Test before launchRequired—

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" }] }
StrategyPick it when
ROUND_ROBIN (default)Spread calls fairly: one agent at a time, longest-idle first, moving on after ring_timeout_seconds
RING_ALLA small team where speed matters: everyone rings, the first to answer takes it
PRIORITYA 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 POST the same JSON body, unchanged, to /v1/webhooks/whatsapp/forward with 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 to overflow_queue_ref (a backup team), or ends as TIMEOUT if 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.

NeedCall
Volumes, answered/abandoned, service level, waitsGET /v1/tenants/acme/reports/calls?from=…&to=…&interval=day
Per agent: answered, missed, talk timeGET /v1/tenants/acme/reports/agents
Right now: waiting, longest wait, agents per queueGET /v1/tenants/acme/reports/live
Call history with filtersGET /v1/tenants/acme/calls?agent_ref=emp-17&from=… (paged)
One call in fullGET /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 OFFLINE is 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.ended reaches 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 ​

SituationResult
An available agent answersTalk; call.answered, then call.ended COMPLETED
The offered agent doesn't answer in ring_timeout_secondsOffered to the next agent (ROUND_ROBIN / PRIORITY)
The agent declinesOffered to the next agent; never back to that one
Nobody availableWaits; offered as soon as someone becomes available
Waited max_wait_secondsMoves to the overflow queue, or ends TIMEOUT
The customer hangs up while waitingCANCELLED or NO_ANSWER

Calloop — the call engine behind your product.