Calls
Every connected conversation is a call with its own ID. Dial sessions create calls for you. You can also place a single call to one number, act on a live call from your app, and read the record afterwards.
Place a single call
A single call dials one number for an agent who is already connected. It runs the same pre-dial checks, detection and recording as a dial session. Use it for click-to-call and scheduled callbacks.
POST/v1/calls
| Parameters | Type | Description |
|---|---|---|
torequired | string | The number to call, E.164. |
from | string | A caller ID you own. Leave it out and set caller_id_pool to have one picked for the lead. |
caller_id_pool | string | Pool to pick the caller ID from. |
agent_idrequired | string | The connected agent who takes the call. |
lead_id | string | Links the call to a lead for calling hours, consent and callback routing. |
amd | boolean | Answering machine detection. Default true. |
amd_on_machine | hangup | notify | What happens when detection hears a machine. Default hangup. |
recording | smart | off | Default smart. |
curl https://api.liveleg.dev/v1/calls \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Idempotency-Key: crm-88214-call-1" \
-d to=+13035550112 \
-d caller_id_pool=pool_local_presence \
-d agent_id=agt_207 \
-d lead_id=led_9Pq1 \
-d amd=true \
-d recording=smartconst res = await fetch('https://api.liveleg.dev/v1/calls', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'crm-88214-call-1',
},
body: JSON.stringify({
to: '+13035550112',
caller_id_pool: 'pool_local_presence',
agent_id: 'agt_207',
lead_id: 'led_9Pq1',
amd: true,
recording: 'smart',
}),
});
const call = await res.json();import os, requests
call = requests.post(
"https://api.liveleg.dev/v1/calls",
headers={
"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}",
"Idempotency-Key": "crm-88214-call-1",
},
json={
"to": "+13035550112",
"caller_id_pool": "pool_local_presence",
"agent_id": "agt_207",
"lead_id": "led_9Pq1",
"amd": True,
"recording": "smart",
},
).json() Control a live call
Each action is its own endpoint on the call. It takes effect right away and sends an event when it completes. The agent's browser or SIP phone can do the same through the Voice SDK, and the event then has by set to agent.
| Endpoint | What it does |
|---|---|
POST /v1/calls/{id}/hold | The prospect hears hold music. The agent can talk to others on the call. |
POST /v1/calls/{id}/unhold | Takes the prospect off hold. |
POST /v1/calls/{id}/mute | Mutes the agent toward the prospect. |
POST /v1/calls/{id}/unmute | Unmutes the agent. |
POST /v1/calls/{id}/dtmf | Sends digits on the prospect's line. |
POST /v1/calls/{id}/transfer | Warm or cold transfer to an agent, an agent pool or a phone number. |
POST /v1/calls/{id}/conference | Adds people to the call. |
DELETE /v1/calls/{id}/participants/{participant_id} | Removes one person from the call. |
POST /v1/calls/{id}/forward | Moves the prospect to another number and frees the agent. |
POST /v1/calls/{id}/hangup | Ends the call for everyone. |
POST /v1/calls/{id}/disposition | Saves the outcome, notes and an optional callback time. |
Hold and mute
Hold plays hold music to the prospect. The agent stays connected and can talk to anyone else on the call. Mute stops the agent's audio toward the prospect while the agent still hears them. Neither needs a body. Each one sends call.updated with the new on_hold and muted values.
POST/v1/calls/{id}/hold
curl -X POST https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/hold \
-H "Authorization: Bearer $LIVELEG_API_KEY"const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/hold', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
},
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/hold",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
).json() {
"id": "evt_6Tc40Hd1",
"type": "call.updated",
"created_at": "2026-11-03T15:44:12.208Z",
"livemode": true,
"data": {
"call_id": "cal_8Jq2mT4x",
"action": "hold",
"on_hold": true,
"muted": false,
"by": "agent"
}
} Send digits
Send key presses on the prospect's line, for example to reach an extension. digits takes 0 to 9, * and #, up to 32 characters. Each w adds a half-second pause.
POST/v1/calls/{id}/dtmf
curl https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/dtmf \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"digits": "3w2#"
}'const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/dtmf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
digits: "3w2#"
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/dtmf",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"digits": "3w2#",
},
).json() Transfer a call
A warm transfer keeps the first agent on the line while the next person joins. A cold transfer hands the call over and frees the first agent for the next call. Targets are agent:, pool: or a phone number.
POST/v1/calls/{id}/transfer
| Parameters | Type | Description |
|---|---|---|
moderequired | warm | cold | warm keeps the first agent on the call until they leave. cold hands the call over right away. |
torequired | string | agent:agt_..., pool:pool_... or a phone number in E.164. |
consult | boolean | Warm only. The prospect is on hold while the two agents talk, and joins when the transfer completes. Default false. |
notes | string | Shown to the receiving agent with the lead details. |
timeout_seconds | integer | How long the target rings. 10 to 60, default 30. |
on_no_answer | return_to_agent | voicemail | hangup | What happens when the target does not answer. Default return_to_agent. |
curl https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/transfer \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "warm",
"to": "agent:agt_closer_09",
"notes": "Wants a roof inspection quote this week.",
"timeout_seconds": 30,
"on_no_answer": "return_to_agent"
}'const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/transfer', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
mode: "warm",
to: "agent:agt_closer_09",
notes: "Wants a roof inspection quote this week.",
timeout_seconds: 30,
on_no_answer: "return_to_agent"
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/transfer",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"mode": "warm",
"to": "agent:agt_closer_09",
"notes": "Wants a roof inspection quote this week.",
"timeout_seconds": 30,
"on_no_answer": "return_to_agent",
},
).json() A warm transfer runs in three steps:
- The target rings. The first agent and the prospect keep talking.
- The target answers and joins the call.
call.transferredarrives with result joined. Withconsult=true, the prospect is on hold while the two agents talk. - The first agent leaves with
POST /v1/calls/{id}/transfer/completeor by hanging up in the SDK. The target has the call, the result is completed, and the first agent is free for the next call.
curl -X POST https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/transfer/complete \
-H "Authorization: Bearer $LIVELEG_API_KEY"const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/transfer/complete', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
},
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/transfer/complete",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
).json() In a cold transfer the first agent is free as soon as the request is accepted. The prospect hears ringing until the target answers. If nobody answers within timeout_seconds, on_no_answer applies. return_to_agent rings the first agent again, or the agent pool when they are on another call. Cancel a transfer before it completes with POST /v1/calls/{id}/transfer/cancel.
| result | Meaning |
|---|---|
joined | Warm. The target answered and is on the call with the first agent and the prospect. |
completed | The first agent left and the target has the call. For cold transfers this is the only success result. |
no_answer | The target did not answer within timeout_seconds. on_no_answer was applied. |
canceled | The first agent canceled before the transfer completed. |
{
"id": "evt_5Mz71Ka8",
"type": "call.transferred",
"created_at": "2026-11-03T15:44:51.733Z",
"livemode": true,
"data": {
"call_id": "cal_8Jq2mT4x",
"mode": "warm",
"from_agent_id": "agt_114",
"to": "agent:agt_closer_09",
"result": "completed"
}
} Add and remove people
Add up to 5 people to a live call: agents, or outside phone numbers such as a manager's mobile. A supervisor added with mode=listen hears everyone and is not heard. Each person gets a participant ID. Joins and departures send call.participant_joined and call.participant_left.
POST/v1/calls/{id}/conference
| Parameters | Type | Description |
|---|---|---|
participantsrequired | array | Up to 5 people. Each has to (agent:agt_... or a phone number in E.164) and mode. |
participants[].mode | talk | listen | listen joins without being heard, for supervisors. Default talk. |
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_closer_09",
"mode": "talk"
},
{
"to": "agent:agt_sup_02",
"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_closer_09",
mode: "talk"
},
{
to: "agent:agt_sup_02",
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_closer_09",
"mode": "talk",
},
{
"to": "agent:agt_sup_02",
"mode": "listen",
},
],
},
).json() {
"call_id": "cal_8Jq2mT4x",
"participants": [
{
"id": "par_1",
"role": "prospect",
"to": "+15125550147",
"mode": "talk",
"status": "connected"
},
{
"id": "par_2",
"role": "agent",
"agent_id": "agt_114",
"mode": "talk",
"status": "connected"
},
{
"id": "par_3",
"role": "agent",
"agent_id": "agt_closer_09",
"mode": "talk",
"status": "ringing"
},
{
"id": "par_4",
"role": "agent",
"agent_id": "agt_sup_02",
"mode": "listen",
"status": "ringing"
}
]
} Remove one person with their participant ID. The call goes on for everyone else. When the last agent leaves, the call ends.
DELETE/v1/calls/{id}/participants/{participant_id}
curl -X DELETE https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/participants/par_4 \
-H "Authorization: Bearer $LIVELEG_API_KEY"const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/participants/par_4', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
},
});
const data = await res.json();import os, requests
data = requests.delete(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/participants/par_4",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
).json() Forward a call
Forward moves the prospect to another phone number and frees the agent at once, with no announcement. Use it to hand a caller to an office line or an answering service. If the number does not answer within timeout_seconds, the call ends. call.transferred arrives with mode forward.
POST/v1/calls/{id}/forward
curl https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/forward \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+15125550108",
"timeout_seconds": 30
}'const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/forward', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: "+15125550108",
timeout_seconds: 30
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/forward",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"to": "+15125550108",
"timeout_seconds": 30,
},
).json() Hang up
Hang-up ends the call for everyone on it. call.completed arrives with hangup_by set to agent. In a dial session the next round starts right away.
POST/v1/calls/{id}/hangup
Dispositions and scheduled callbacks
A disposition records the outcome of a call. Codes are your own strings. Send it during the call or after it ends, and it shows on the call record and on call.completed. Set callback_at to schedule a callback with the same agent. callback.scheduled confirms it. How callbacks run is in Scheduled callbacks.
POST/v1/calls/{id}/disposition
| Parameters | Type | Description |
|---|---|---|
coderequired | string | Your outcome code, for example callback_requested or sold. |
notes | string | Free text, up to 2,000 characters. |
callback_at | string | ISO 8601 with offset. Schedules a callback to the lead at that time. |
curl https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/disposition \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "callback_requested",
"notes": "Owner on a job. Call Thursday after 2 pm.",
"callback_at": "2026-11-06T14:00:00-06:00"
}'const res = await fetch('https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/disposition', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: "callback_requested",
notes: "Owner on a job. Call Thursday after 2 pm.",
callback_at: "2026-11-06T14:00:00-06:00"
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/calls/cal_8Jq2mT4x/disposition",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"code": "callback_requested",
"notes": "Owner on a job. Call Thursday after 2 pm.",
"callback_at": "2026-11-06T14:00:00-06:00",
},
).json() Inbound callbacks
When a lead calls back one of your numbers, the call rings the agent who last spoke with them, then the agent pool, then voicemail. Routing settings, hold, the opt-out menu and inbound prices are in Inbound calls and callbacks.
Call records
A call record lists every leg placed for the call, answered or not, with timings, the detection result, billed seconds and cost. Filter by dial session, agent, lead or date range. The cost fields are explained in Cost per call.
GET/v1/call_records
curl "https://api.liveleg.dev/v1/call_records?dial_session_id=dls_31Ka9&limit=50" \
-H "Authorization: Bearer $LIVELEG_API_KEY" {
"id": "cal_8Jq2mT4x",
"object": "call",
"dial_session_id": "dls_31Ka9",
"account_id": "acc_ridgeline",
"direction": "outbound",
"from": "+15125550199",
"to": "+15125550147",
"agent_id": "agt_114",
"status": "completed",
"legs": [
{
"id": "leg_a1",
"to": "+15125550147",
"result": "bridged",
"rang_ms": 4120
},
{
"id": "leg_a2",
"to": "+15125550163",
"result": "canceled",
"rang_ms": 15400
},
{
"id": "leg_a3",
"to": "+15125550108",
"result": "machine_voicemail",
"rang_ms": 9800
}
],
"answered_at": "2026-11-03T15:42:03.542Z",
"bridged_at": "2026-11-03T15:42:07.412Z",
"ended_at": "2026-11-03T15:46:29.903Z",
"amd_result": "human",
"speech_ended_to_bridge_ms": 980,
"billed_seconds": 264,
"hangup_by": "prospect",
"disposition": "callback_requested",
"rate_per_minute": "0.12",
"amount": "0.528",
"charges": [
{"item": "outbound_minutes", "quantity": 4.4, "amount": "0.528"}
],
"surcharges": [],
"total": "0.528"
}