Agent calling
We host the agent's side of the call. Agents talk from the browser or a SIP phone, stay connected for the whole dial session, and each live person is bridged into that open line.
Agents and pools
An agent is one person who takes calls. Create one per seat and put agents in pools when they share calls from one dial session. A disconnected agent gets no calls. Their session pauses until they reconnect. agent.connected and agent.disconnected report each change on your webhook.
POST/v1/agents
| Parameters | Type | Description |
|---|---|---|
namerequired | string | Shown in the dashboard and on call records. |
agent_pool_id | string | The pool the agent shares calls in. Created if it does not exist. |
sip | boolean | Give the agent a SIP address and credentials. Default false. |
email | string | The agent's work email. |
metadata | object | Up to 20 keys. Returned on events about the agent. |
curl https://api.liveleg.dev/v1/agents \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Jordan Pike",
"agent_pool_id": "pool_closers",
"sip": false
}'const res = await fetch('https://api.liveleg.dev/v1/agents', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: "Jordan Pike",
agent_pool_id: "pool_closers",
sip: false
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/agents",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"name": "Jordan Pike",
"agent_pool_id": "pool_closers",
"sip": False,
},
).json() Tokens
The browser and the mobile libraries connect with a short-lived token for one agent. Mint it on your server with your API key and pass it to the page. A token lasts up to 1 hour and works for that agent only. It is checked when the connection opens, and an open connection stays up for the whole shift. Reconnecting needs a new token.
POST/v1/agents/{id}/tokens
curl https://api.liveleg.dev/v1/agents/agt_114/tokens \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-d ttl_seconds=3600const res = await fetch('https://api.liveleg.dev/v1/agents/agt_114/tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ttl_seconds: 3600 }),
});
const token = await res.json();import os, requests
token = requests.post(
"https://api.liveleg.dev/v1/agents/agt_114/tokens",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={"ttl_seconds": 3600},
).json() {
"token": "eyJhbGciOiJFUzI1NiJ9.eyJhZ3QiOiJhZ3RfMTE0In0.Xq9...",
"agent_id": "agt_114",
"expires_at": "2026-11-03T15:00:02Z"
} Browser
Install @liveleg/voice from npm and call Agent.connect(token, options). It checks the microphone and network, then opens one audio connection over WebRTC that stays open between calls. Bridged calls arrive on that connection. The agent does not press answer and hears no ring. Time spent connected and waiting is never billed.
import { Agent } from '@liveleg/voice';
// Your server mints the token. Never put an API key in the browser.
const getToken = () => fetch('/api/liveleg-token').then((r) => r.json()).then((d) => d.token);
const agent = await Agent.connect(await getToken(), {
getToken,
inputDeviceId: savedMicId,
});
agent.on('call.bridged', (call) => {
showLead(call.leadId, call.to);
});
agent.on('call.ended', (call) => openWrapUp(call.id));
agent.on('network.quality', ({ level }) => setSignal(level));
agent.on('disconnected', ({ reason }) => showOffline(reason));
document.querySelector('#mute').onclick = () => agent.mute();
document.querySelector('#hold').onclick = () => agent.hold();
document.querySelector('#hangup').onclick = () => agent.hangup(); Everything the SDK does on a call is also available on the Calls API. Your server can act on the same call at any time.
Connect options
Agent.connect returns a promise for the connected agent. It rejects with a code when the agent cannot connect.
| Option | Type | Description |
|---|---|---|
getToken | function | Returns a promise for a fresh token. Called when the SDK has to reconnect. |
inputDeviceId | string | Microphone to use. Default is the system microphone. |
outputDeviceId | string | Speaker or headset to use. Default is the system output. |
autoReconnect | boolean | Reconnect after a network drop. A bridged call stays up for 30 seconds while the SDK reconnects. Default true. |
echoCancellation | boolean | Default true. |
noiseSuppression | boolean | Default true. |
logLevel | error | warn | info | debug | Console logging. Default warn. |
try {
await Agent.connect(token);
} catch (err) {
// err.code: microphone_denied, no_microphone, invalid_token or network_blocked
showSetupHelp(err.code);
} SDK events
Listen with agent.on(event, handler) and stop with agent.off(event, handler). These events go to the page only. Your server gets the matching webhooks, such as call.bridged and agent.disconnected, from Webhooks and events.
| Event | Sent when | Payload |
|---|---|---|
call.bridged | A live person is connected. The agent hears them right away. | id, leadId, externalId, to, from, dialSessionId, ivrPath, holdSeconds |
call.ended | The bridged call ends. | id, hangupBy, durationSeconds |
session.paused | The agent's dial session is paused. | dialSessionId, reason |
session.resumed | The dial session starts dialing again. | dialSessionId |
device.changed | A microphone or speaker is plugged in, removed or selected. | kind (input, output), deviceId, label |
device.error | The microphone is blocked or lost. | code (microphone_denied, device_lost), message |
network.quality | Every 5 seconds during a call. | level (good, fair, poor), rttMs, jitterMs, packetLoss |
network.reconnecting | The connection dropped and the SDK is reconnecting. | attempt |
network.reconnected | The connection is back. | |
disconnected | The connection closed. The dial session pauses. | reason (disconnect, network, token_expired, replaced) |
SDK methods
Call methods act on the current call. agent.call holds the current call, or null between calls. agent.muted and agent.onHold hold the current state.
| Method | What it does |
|---|---|
mute() | Mutes the agent's microphone toward the lead. |
unmute() | Turns the microphone back on. |
hold() | The lead hears hold music. Same as hold on the Calls API. |
resume() | Takes the lead off hold. Same as unhold on the Calls API. |
sendDigits(digits) | Sends DTMF on the lead's line. Digits 0 to 9, * and #, up to 32 characters. Each w adds a half-second pause. |
transfer({ to, mode, timeoutSeconds }) | Warm or cold transfer to agent:, pool: or a phone number. Same as the Calls API transfer. |
hangup() | Ends the current call. In a dial session, the next round starts. |
setInputDevice(deviceId) | Switches the microphone, also during a call. |
setOutputDevice(deviceId) | Switches the speaker or headset. |
disconnect() | Closes the connection. The dial session pauses until the agent connects again. |
Mobile apps
Libraries for iOS and Android are available on request. They use the same tokens, events and methods as the browser SDK.
- During a dial session the app keeps the connection open in the foreground, the same as the browser.
- The library registers the device for push notifications. When a callback or a transfer rings an agent whose app is in the background, a push wakes the app and it shows the phone's own incoming call screen.
- Bluetooth headsets and switching between speaker and earpiece are handled by the library.
SIP phones
Create the agent with sip=true to get a SIP address and credentials. Each agent has its own address. Register a desk phone, a softphone or your phone system to it. The password is shown once.
{
"id": "agt_114",
"sip": {
"address": "sip:agt_114@sip.liveleg.dev",
"transport": "tls",
"port": 5061,
"media": "srtp",
"codecs": ["opus", "g722", "pcmu", "pcma"],
"username": "agt_114",
"password": "shown once"
}
} - Signaling over TLS on port 5061 and media over SRTP. Unencrypted connections are refused.
- Codecs: Opus, G.722 and G.711 (PCMU and PCMA).
- One active registration per agent. A new registration replaces the old one and sends
agent.disconnectedwith reasonreplaced. - When a dial session starts, we call the agent's SIP address once. The agent answers and that call stays open for the whole session. Each live person is bridged into it with no ring. Hanging up the phone pauses the session.
- Single calls and callbacks to an agent who is not in a session ring the phone as a normal call.
- Keys pressed on the phone are sent to the lead's line. Transfers and dispositions go through the Calls API.
Create new credentials at any time. The old password stops working right away.
POST/v1/agents/{id}/sip_credentials
curl -X POST https://api.liveleg.dev/v1/agents/agt_114/sip_credentials \
-H "Authorization: Bearer $LIVELEG_API_KEY" Supervisor listening
A supervisor listens to a live call without being heard by the agent or the lead. The supervisor is an agent connected in the browser or on a SIP phone, added to the call as a participant with mode=listen. Remove them with DELETE /v1/calls/{id}/participants/{participant_id}, or listening ends when the call ends. Details are in Add and remove people.
POST/v1/calls/{id}/conference
curl https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/conference \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"participants": [
{
"to": "agent:agt_201",
"mode": "listen"
}
]
}'const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/conference', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
participants: [
{
to: "agent:agt_201",
mode: "listen"
}
]
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/conference",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"participants": [
{
"to": "agent:agt_201",
"mode": "listen",
},
],
},
).json() Media streams
Send live call audio to your own WebSocket server, for coaching tools, live notes or your own speech models. Audio is 16 kHz, 16-bit mono PCM, base64 in JSON messages, with agent and prospect on separate tracks. Choose agent, prospect or both in tracks. Set content=transcript to receive the live transcript on the same endpoint instead of audio. The stream closes when the call ends.
POST/v1/calls/{id}/streams
curl https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/streams \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-d url=wss://coach.example.com/audio \
-d tracks=bothconst res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/streams', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: 'wss://coach.example.com/audio', tracks: 'both' }),
});
const stream = await res.json();import os, requests
stream = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/streams",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={"url": "wss://coach.example.com/audio", "tracks": "both"},
).json() {
"event": "media",
"call_id": "cal_8Jq2mT4x",
"track": "prospect",
"sequence": 412,
"timestamp_ms": 8240,
"payload": "AAD+/wIA..."
}