Appearance
4. Step by step
The steps every product goes through, in order. Your scenario page repeats them with the exact requests for your kind of product.
| Step | Required? | The essential calls |
|---|---|---|
| 4.0 Credentials | Required | from the operator |
| 4.1 The API | Required | API key on every request |
| 4.2 Your organisation | Required | PUT /v1/tenants/{t}, PUT …/agents/{ref}, PUT …/channels/{ref}; a queue for shared lines |
| 4.3 Events | Recommended | PUT /v1/webhook; the lookup hook is optional |
| 4.4 Agent tokens | Required | your token endpoint |
| 4.5 The app | Required | the SDK's connect |
| 4.6 Push | Required for mobile apps | PUT /v1/push-credentials/*, PUT …/push-tokens/{deviceId} |
| 4.7 Outbound calls | Only if you call out | POST /v1/tenants/{t}/calls + the SDK's startOutbound |
| 4.8 History, reports, recordings | Optional | GET …/calls, GET …/reports/*, GET …/recording |
| 4.9 Testing | Required | — |
| 4.10 The dashboard | Optional | your own staff's accounts; a launch link for each business's staff |
4.0 Get your credentials from the Calloop operator
Required.
The Calloop operator is the Calloop team that runs the platform. Ask them for:
Your consumer. They create it and send you, once:
- an API key
ck_yourproduct_…, for your backend; - a signing key (
kid+ secret), for signing agent tokens; - a webhook secret, for verifying events, only if they set a webhook or lookup URL when creating your consumer. Otherwise the first
PUT /v1/webhookthat sets a URL returns it, once (§4.3).
Store them in your secrets manager. Calloop can't show them again; you can issue new ones yourself later (Keys).
- an API key
Calloop's addresses:
https://api.calloop.net: the API, agent sockets and the MCP server. It's your backend's base URL and the agent SDKs'url.https://app.calloop.net: your dashboard, and where dashboard launch links go (§4.10).https://docs.calloop.net/docs/: these docs.
For SIP lines: a trunk. Either one for your own carrier — you add it yourself (dashboard → SIP trunks, or
POST /v1/sip-trunks: its host, the public addresses it sends calls from, its number format, any digest credentials), submit it, and the operator approves it (unless they set your account to add trunks for you, or to take them live at once); only your lines can use it — or a platform trunk the operator shares. Point your carrier at the trunk'singressHost(sip:<number>@<ingressHost>). Each SIP line names its trunk by id:GET /v1/sip-trunkslists the ones you can use (your dashboard's line form offers them). Different lines can be on different trunks. Optional: if your carrier puts a code in front of the number (a tech prefix) to say what each call is for, or wants one in front of the numbers you dial, set the trunk'stech_prefixes(dashboard → the trunk's Tech prefixes, or Tech prefixes). Calls marked for processing reach your lines; calls marked for forwarding or filtering go to the forwarding rules or call filters the operator sets up for you. Optional: give your own trunk your carrier's price lists (dashboard → Pricing & rate sheets, then the trunk's Carrier sheets) and Calloop prices its calls for you — your carrier's cost in Usage, and your tenants' prices can follow it (Your trunks' carrier sheets).
4.1 Call the API from your backend
Required.
Every request carries the API key and is scoped to your consumer:
bash
curl -X PUT https://api.calloop.net/v1/tenants/biz-123 \
-H "Authorization: Bearer ck_yourproduct_..." \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Ltd", "settings": { "team_view": true, "monitoring": { "modes": ["listen","whisper","barge"] } } }'Rules worth knowing from day one (Conventions):
PUTcreates or replaces. A field you leave out goes back to its default. Send the whole object every time. The exceptions, kept when left out: a tenant'ssettings, an agent'sroleand a line'scredentials. Note that a tenant'sstatusis not kept: a tenantPUTwithout it sets the tenantACTIVE, so it un-suspends a suspended one.- Retry
POSTs with anIdempotency-Keyheader (a UUID per logical request), so a retry never creates a second call. - Errors always look like
{ "error": { "code": "…", "message": "…" } }. A400about one field adds"details": { "field": "…" }— the field as you sent it (e.g.name,members[2].priority), so your forms can show the message next to it. - Rate limit: 1200 requests per minute per consumer (
429withRetry-After).
4.2 Set up your organisation
Required: a tenant, agents and at least one line (plus a queue for a shared line). IVR and overflow are optional.
Contact center: agents, a queue, and a line routed into it:
http
PUT /v1/tenants/biz-123/agents/emp-17 { "name": "Rana", "role": "AGENT" }
PUT /v1/tenants/biz-123/agents/mgr-2 { "name": "Omar", "role": "SUPERVISOR" }
PUT /v1/tenants/biz-123/queues/support { "name": "Support", "strategy": "ROUND_ROBIN", "ring_timeout_seconds": 20, "max_wait_seconds": 180 }
PUT /v1/tenants/biz-123/queues/support/members { "members": [{ "agent_ref": "emp-17" }] }
PUT /v1/tenants/biz-123/channels/wa-main { "type": "WHATSAPP", "address": "+442079460000",
"provider_account_id": "<phone_number_id>",
"credentials": { "access_token": "<Meta token>" },
"inbound_queue_ref": "support" }Pick the queue strategy:
| Strategy | Use when |
|---|---|
ROUND_ROBIN (default) | Spread calls fairly: one agent at a time, longest-idle first, passing on after ring_timeout_seconds |
RING_ALL | Small teams where speed matters: everyone rings, first to answer takes it |
PRIORITY | Senior agents or a main team first, others as backup (priority per member) |
Connect the WhatsApp number (once per number, the choice from §3.3):
- Forward: in the handler where your backend receives Meta's webhook, also
POSTthe same JSON body, unchanged, to/v1/webhooks/whatsapp/forwardwith your API key. Do it for every payload; Calloop picks out the calls on your own lines and ignores the rest. - Direct: ask the Calloop operator to point the Meta app's webhook at
https://api.calloop.net/webhooks/whatsapp.
Until one of these is in place, the line exists but no WhatsApp call reaches it. SIP lines need nothing more: the carrier already sends the DID's calls to Calloop's trunk (§4.0).
Add an IVR menu only if callers must choose a department (IVR flows); add overflow_queue_ref to send long waits to a backup queue.
Personal lines: at each user's signup, two calls:
http
PUT /v1/tenants/myapp/agents/user-7 { "name": "Jane" }
PUT /v1/tenants/myapp/channels/line-7 { "type": "SIP", "address": "+442079460456",
"sip_trunk_id": 1, "owner_agent_ref": "user-7",
"ring_timeout_seconds": 30 }What happens on a personal line: a call rings the owner on all their devices, whatever their availability. If the owner is already on a call, the caller hears busy. With no device to ring, or no answer within the ring timeout, the call ends as NO_ANSWER, and your webhook gets call.ended naming the owner and the line, so you can show a missed call. Only the owner may call out from their line.
Managing users' lines over time:
- Do not disturb: when a user mutes calls in your app, your backend calls
PUT /v1/tenants/myapp/agents/user-7/do-not-disturb { "enabled": true }(andfalseto undo). Calls to their line then ring nothing and arrive as missed calls (NO_ANSWER). - Your number inventory:
GET /v1/tenants/myapp/channels?unowned=true&type=SIPlists numbers not yet given to anyone,?owner_agent_ref=user-7a user's lines and?address=+44…finds a number. Addlimitandafter_idto page through thousands. - Moving a number:
PUTthe line again with the newowner_agent_ref. - A user leaves or is suspended:
DELETE /v1/tenants/myapp/agents/user-7. Their app is disconnected at once and their tokens stop working, even unexpired ones; only a newPUTof the agent lets them back. Free their number byPUTting the line without an owner, orDELETEit.
A personal line on a tenant with listen-only monitoring takes Calloop's direct media path (recorded or not) for inbound calls and outbound SIP calls: about 70 times cheaper in media CPU than a room call, with no feature loss for a one-to-one call. Outbound WhatsApp calls on such a line use a room.
4.3 Receive events
Recommended (needed for call history, missed calls and billing in your own system). The lookup hook at the end is optional.
- Expose an
https://endpoint, then register it, optionally with only the events you need:httpThe first time you set a URL (and you got no webhook secret from the operator), the response carries the secret, once — store it in your secrets manager:PUT /v1/webhook { "url": "https://api.example.com/calloop/events", "event_types": ["call.created", "call.answered", "call.ended", "recording.completed"] }json{ "webhook": { "url": "https://api.example.com/calloop/events", "lookupUrl": null, "eventTypes": ["call.created", "call.answered", "call.ended", "recording.completed"], "secretSet": true }, "secret": "…" }PUT /v1/webhookreplaces all your webhook settings: a URL you leave out is removed, and leaving outevent_typesmeans every event type. - Verify every request (
X-Calloop-Signature, HMAC-SHA256 with your webhook secret over"<t>.<raw body>"): events.md → Verifying requests. - Answer
2xxfast, then process. Anything else, or a timeout after 10 s, is retried with backoff (5 s, 15 s, 1 min, 5 min, 15 min, 30 min, then hourly) and given up after the 12th attempt, about 6 hours later. - De-duplicate on
event_id: delivery is at least once. - Don't rely on order: each event carries the whole current call, so keep the one with the latest
occurred_at. - Missed some during an outage?
GET /v1/eventslists them andPOST /v1/events/{event_id}/redeliver(the event'sevent_idUUID) sends one again.
Most products need only call.ended (the full record: who, when, how long, why it ended) and recording.completed. Add call.created / call.answered for live status in your UI.
Optional lookup hook. Send lookup_url in the same PUT /v1/webhook, together with your url and event_types (a PUT without them would remove them), to have Calloop ask you about each inbound caller before ringing, within 1.5 s. Reply with a name to show, your contact id (external_ref), or "action": "reject" to block a caller. If you're slow or down, the call continues without it. See events.md → Lookup hook.
4.4 Sign agent tokens
Required.
Your app connects to Calloop with a JWT your backend signs for the logged-in user. Add an endpoint like GET /api/calloop-token that returns one (as plain text in the examples below; any format works as long as your app's getToken returns the token string):
| Claim | Value |
|---|---|
header alg / kid | HS256 / your signing key id (e.g. k1) |
iss | your consumer slug |
tnt | the tenant ref |
sub | the user's agent ref |
exp | required; keep it short (5–15 minutes). The SDK fetches a fresh one on every reconnect |
name, role | display name; AGENT or SUPERVISOR. Applied to the agent on every connect: send name always (without it the agent is renamed to its ref), and a role that matches the agent's PUT (a role in the token replaces it) |
Node (jsonwebtoken):
js
const token = jwt.sign(
{ iss: 'yourproduct', tnt: tenantRef, sub: user.agentRef, name: user.name, role: 'AGENT' },
process.env.CALLOOP_SIGNING_SECRET,
{ algorithm: 'HS256', keyid: process.env.CALLOOP_SIGNING_KID, expiresIn: '10m' },
);PHP / Laravel (firebase/php-jwt):
php
$token = JWT::encode([
'iss' => 'yourproduct', 'tnt' => $tenantRef, 'sub' => $user->agent_ref,
'name' => $user->name, 'role' => 'AGENT', 'exp' => time() + 600,
], env('CALLOOP_SIGNING_SECRET'), 'HS256', env('CALLOOP_SIGNING_KID'));Only return a token for the logged-in user, for their own tenant. Never put the signing secret in the app. Calloop creates the agent on first connect if you haven't yet. Tokens without exp are refused.
4.5 Build the app
Required.
Web (JS SDK):
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()),
// deviceId: optional; by default the SDK keeps one per browser in localStorage
// and exposes it as agent.deviceId
});
agent.setAvailability('AVAILABLE'); // contact center only; personal lines ring regardless
agent.on('incoming', (call) => {
showRinging(call.customer);
call.on('remoteStream', (s) => { audioEl.srcObject = s; });
acceptBtn.onclick = () => call.accept();
declineBtn.onclick = () => call.decline();
});Flutter (Dart SDK):
dart
final agent = await CalloopAgent.connect(
url: 'https://api.calloop.net',
deviceId: storedDeviceId, // stable per installation
getToken: () => api.fetchCalloopToken(),
);
agent.onIncoming.listen((call) => showRinging(call.customer));For the phone's native incoming-call screen (Android ConnectionService, iOS CallKit) and handling pushes in the background, see the Dart SDK → native call screen.
The SDKs already handle reconnects, page reloads, a call moving between the user's devices, ICE/TURN servers (they come from Calloop) and resyncing after a push. Your app only draws screens and reacts to events.
Supervisors use the same SDK: the live board (agent.subscribeBoard(...) with filters, paged calls, counters) and monitoring (listen; whisper and barge where the tenant allows). See the "Supervisors" section of the JS and Dart READMEs, and "Supervisor screen" in the React one.
4.6 Push notifications
Required for a mobile app (a phone in the background only rings through push); not needed for a browser-only app.
- Upload your credentials once:
PUT /v1/push-credentials/fcm,…/apns,…/onesignal. - When the app gets a device token, send it to your backend, which registers it:httpiOS registers two:
PUT /v1/tenants/myapp/agents/user-7/push-tokens/<deviceId> { "platform": "ANDROID", "provider": "FCM", "token": "<fcm token>" }APNS_VOIP(the PushKit token) for calls, andFCMfor the banner. - On logout,
DELETE …/push-tokens/<deviceId>.
Use the same deviceId here as the SDK connects with: the one you pass to connect, or, in a browser where you passed none, agent.deviceId.
4.7 Outbound calls
Only if you call out.
Outbound always starts on your backend, because checking consent to call a customer is your responsibility:
http
POST /v1/tenants/biz-123/calls
Idempotency-Key: 6f1c…
{ "channel_ref": "wa-main", "agent_ref": "emp-17",
"customer": { "address": "+447700900123", "name": "Jane Doe" },
"external_ref": "crm-call-42" }The response is 201 { call }. Hand call.callId to that agent's app, which calls agent.startOutbound(callId); Calloop then dials the customer. An intent not started within 2 minutes is cancelled. On a personal line only its owner may call out.
4.8 History, reports, recordings, deletion
Optional: use what you need.
| Need | Use |
|---|---|
| Call history in your UI | Store call.ended events, or query GET /v1/tenants/{t}/calls (filters: agent, line, queue, customer, dates; paged) |
| One call in full | GET /v1/calls/{id}: participants, legs, lifecycle log, transfers, IVR inputs, recording |
| Dashboards | GET /v1/tenants/{t}/reports/calls (volumes, answered/abandoned, service level, waits), …/reports/agents, …/reports/live |
| Link a call to your records | external_ref and consumer_metadata at creation, by the lookup hook, or later with PATCH /v1/calls/{id} |
| Play a recording | GET /v1/calls/{id}/recording: a short-lived URL to the Ogg file (left channel = customer, right = agent) |
| A customer asks to be forgotten | POST /v1/tenants/{t}/customers/erase { address } |
| Delete one call | DELETE /v1/calls/{id} |
4.9 Test before going live
Required.
- Test tokens: sign one with your signing key, as your token endpoint will (the agent token), or ask the operator for one.
- A softphone, to call your SIP lines: add a trunk whose source address is the softphone's public IP (
POST /v1/sip-trunks,inbound_source_cidrs), or send that IP to the operator and they add one. Then dialsip:<line number>@<ingressHost>. - Webhook: watch
GET /v1/eventsto see what Calloop sent and how your endpoint answered.
4.10 Give your people the dashboard
Optional: instead of building every admin screen yourself.
Calloop's dashboard (https://app.calloop.net/app) has the screens a call center runs on: agents, queues and their members, lines, a visual IVR builder, audio, the live board with listening, calls with recordings, and reports. It works on the same API you use, so anything set up in it is what your backend reads, and the other way round.
| Who | How they get in | What they see |
|---|---|---|
| Your own staff | A dashboard account (email, password, two-factor). Ask the Calloop operator for your first owner; owners add the rest from Users. | Every tenant; owners also your keys, webhook and push credentials, and the audit log |
| A business's staff (each tenant's call-center admin or supervisor) | Your product sends them in: a "Call center" button calls your backend, which signs a short-lived launch token and redirects to /app/launch#token=… (Dashboard launch). No Calloop password: your users and roles decide. | Their one tenant: ADMIN sets it up and supervises, SUPERVISOR watches, listens and reports |
The launch token is signed with the same signing key as agent tokens (4.4), with aud: "calloop-dashboard", the tenant, the user's id and name, the role and a unique jti; it lives at most 5 minutes and works once. Everything done in the dashboard is in your audit log (Audit log, owners).