Dial sessions
A dial session calls a lead list for one agent or a pool of agents, up to 5 lines per agent at a time. The first live person who answers is connected to the agent and the other lines are dropped.
How a session runs
Each round, we take the next leads from the list and run the pre-dial checks on each one: number lookup, your do-not-call list, calling hours in the lead's local time, state call limits and consent. Leads that pass are dialed at the same time, each from a local caller ID. Leads that fail get a call.blocked event with the rule and the next time they can be called.
Answering machine detection listens on every answered line. When it hears a person, that line is bridged to the agent and the rest are canceled. The agent hears the person about 1 second after they finish saying hello. When the call ends, the next round starts.
- Every line rings for at least 15 seconds or 4 rings before it is dropped.
- If two people answer and no agent is free, the second hears your identified message with a press-1 opt-out. That call counts toward the abandonment cap.
- With
lines=auto, the line count drops when the campaign's abandonment rate nears the cap and rises when fewer people answer.
Campaigns
A campaign groups the lead lists and dial sessions that call for one offer. The 30-day abandoned-call rate and the 3% cap are tracked per campaign. Calling hours, attempts per lead and the retry wait are also set per campaign.
POST/v1/campaigns
| Parameters | Type | Description |
|---|---|---|
namerequired | string | Shown in the dashboard and on reports. |
id | string | Your own ID, starting with cmp_. Generated if you leave it out. |
calling_hours | object | days, start and end in the lead's local time, for example mon to fri, 09:00 to 18:00. Narrows the state and account rules. It never widens them. |
max_attempts | integer | Calls to a lead without reaching a person before the lead is exhausted. 1 to 20, default 6. |
retry_after_minutes | integer | Wait after an unanswered call before the lead is dialed again. Default 240. |
caller_id_pool | string | Default pool for sessions in this campaign. |
max_lines | integer | Upper limit on lines per agent for every session in the campaign. 1 to 5, default 5. |
require_consent | boolean | Skip leads without a consent record. Defaults to the account compliance setting. |
abandon_message_id | string | The identified message played on abandoned calls. Defaults to the account message. |
metadata | object | Up to 20 keys. Returned on events for the campaign. |
curl https://api.liveleg.dev/v1/campaigns \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Roofing Q4",
"calling_hours": {
"days": [
"mon",
"tue",
"wed",
"thu",
"fri"
],
"start": "09:00",
"end": "18:00"
},
"max_attempts": 6,
"retry_after_minutes": 240,
"caller_id_pool": "pool_local_presence"
}'const res = await fetch('https://api.liveleg.dev/v1/campaigns', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: "Roofing Q4",
calling_hours: {
days: [
"mon",
"tue",
"wed",
"thu",
"fri"
],
start: "09:00",
end: "18:00"
},
max_attempts: 6,
retry_after_minutes: 240,
caller_id_pool: "pool_local_presence"
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/campaigns",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"name": "Roofing Q4",
"calling_hours": {
"days": ["mon", "tue", "wed", "thu", "fri"],
"start": "09:00",
"end": "18:00",
},
"max_attempts": 6,
"retry_after_minutes": 240,
"caller_id_pool": "pool_local_presence",
},
).json() Calling hours on a campaign use the lead's local time. The state rules in Compliance and your account settings still apply. A campaign window of 8 am to 9 pm is cut to 9 am to 7 pm for Pennsylvania leads.
Attach a campaign by setting campaign_id on the lead list. A dial session uses the list's campaign, or the campaign_id you pass when you start it.
Read the campaign to see its abandonment numbers. line_limit is normal or one_per_agent. At 3%, every session in the campaign dials one line per agent until the 30-day rate is back under the cap. Dialing does not stop. You get compliance.cap_reached and an email, then compliance.cap_cleared when normal line counts return.
GET/v1/campaigns/{id}
curl https://api.liveleg.dev/v1/campaigns/cmp_roofing_q4 \
-H "Authorization: Bearer $LIVELEG_API_KEY"const res = await fetch('https://api.liveleg.dev/v1/campaigns/cmp_roofing_q4', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
},
});
const data = await res.json();import os, requests
data = requests.get(
"https://api.liveleg.dev/v1/campaigns/cmp_roofing_q4",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
).json() {
"id": "cmp_roofing_q4",
"object": "campaign",
"name": "Roofing Q4",
"calling_hours": {"days": ["mon", "tue", "wed", "thu", "fri"], "start": "09:00", "end": "18:00"},
"max_attempts": 6,
"retry_after_minutes": 240,
"caller_id_pool": "pool_local_presence",
"max_lines": 5,
"abandonment": {
"rate_30d": 0.012,
"answered_30d": 4120,
"abandoned_30d": 49,
"cap": 0.03,
"line_limit": "normal"
},
"created_at": "2026-10-01T15:10:44Z"
} Lead lists
A lead list holds the leads a session dials. Each list belongs to one campaign. One list can feed several sessions at once, and each lead is dialed by one session at a time.
POST/v1/lead_lists
| Parameters | Type | Description |
|---|---|---|
id | string | Your own ID, starting with lst_. Generated if you leave it out. |
namerequired | string | Shown in the dashboard. |
campaign_id | string | The campaign the list belongs to. A list created without one gets a campaign of its own. |
skip_duplicates | boolean | Skip a lead whose phone is already in the list. Default true. |
metadata | object | Up to 20 keys. |
curl https://api.liveleg.dev/v1/lead_lists \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "lst_roofing_tx",
"name": "Roofing, Texas",
"campaign_id": "cmp_roofing_q4"
}'const res = await fetch('https://api.liveleg.dev/v1/lead_lists', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: "lst_roofing_tx",
name: "Roofing, Texas",
campaign_id: "cmp_roofing_q4"
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/lead_lists",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"id": "lst_roofing_tx",
"name": "Roofing, Texas",
"campaign_id": "cmp_roofing_q4",
},
).json() {
"id": "lst_roofing_tx",
"object": "lead_list",
"name": "Roofing, Texas",
"campaign_id": "cmp_roofing_q4",
"skip_duplicates": true,
"counts": {"queued": 0, "scheduled": 0, "in_call": 0, "completed": 0, "exhausted": 0, "held": 0, "do_not_call": 0, "invalid": 0},
"created_at": "2026-11-02T20:14:09Z"
} Load leads
Add up to 500 leads per request. You can keep adding leads while a session is running. For larger imports, send several requests, each with its own Idempotency-Key.
POST/v1/lead_lists/{id}/leads
| Lead fields | Type | Description |
|---|---|---|
phonerequired | string | E.164 format, US numbers only. |
staterequired | string | Two-letter state. Required unless the area code maps to one state. Leads with no state are never dialed automatically. |
name | string | Shown to the agent when the call connects. |
company | string | Shown to the agent. |
external_id | string | Your CRM ID. Returned on every event about the lead. |
consent_id | string | A consent record. Required when the campaign has require_consent on. |
callback_at | string | A time to call this lead, ISO 8601. Moves to the next open window if it falls outside calling hours. |
ivr | object | Phone tree target for this lead. Overrides the session default. See phone tree targets. |
metadata | object | Up to 20 keys. Returned on every event about the lead. |
curl https://api.liveleg.dev/v1/lead_lists/lst_roofing_tx/leads \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"phone": "+15125550147",
"state": "TX",
"name": "Dana Ruiz",
"company": "Ridgeline Roofing",
"external_id": "crm_88213"
},
{
"phone": "+17205550126",
"state": "CO",
"name": "Sam Hollis",
"company": "Hollis Family Dental",
"external_id": "crm_88214"
}
]
}'const res = await fetch('https://api.liveleg.dev/v1/lead_lists/lst_roofing_tx/leads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
leads: [
{
phone: '+15125550147',
state: 'TX',
name: 'Dana Ruiz',
company: 'Ridgeline Roofing',
external_id: 'crm_88213',
},
{
phone: '+17205550126',
state: 'CO',
name: 'Sam Hollis',
company: 'Hollis Family Dental',
external_id: 'crm_88214',
},
],
}),
});
const data = await res.json();import os, requests
data = requests.post(
"https://api.liveleg.dev/v1/lead_lists/lst_roofing_tx/leads",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
json={
"leads": [
{
"phone": "+15125550147",
"state": "TX",
"name": "Dana Ruiz",
"company": "Ridgeline Roofing",
"external_id": "crm_88213",
},
{
"phone": "+17205550126",
"state": "CO",
"name": "Sam Hollis",
"company": "Hollis Family Dental",
"external_id": "crm_88214",
},
],
},
).json() {
"object": "lead_batch",
"lead_list_id": "lst_roofing_tx",
"added": 2,
"duplicates": 0,
"rejected": [],
"data": [
{"id": "led_44Hc2", "external_id": "crm_88213", "status": "queued"},
{"id": "led_44Hc3", "external_id": "crm_88214", "status": "queued"}
]
} Valid leads are added even when others in the same request fail. Each failed lead is listed in rejected with its position in the request and a code: invalid_phone, not_us_number or missing_phone. Leads whose phone is already in the list are counted in duplicates and skipped.
"rejected": [
{"index": 7, "phone": "+1512555014", "code": "invalid_phone"},
{"index": 12, "phone": "+447700900123", "code": "not_us_number"}
] Change a lead with PATCH /v1/leads/{id} using the same fields. DELETE /v1/leads/{id} removes it from the list. Its call history stays.
Lead statuses
Each lead has a status. For a lead stopped by a pre-dial check, status_reason is the reason from call.blocked. Every change sends lead.status_changed. Filter the list by status to see what is left to dial.
GET/v1/lead_lists/{id}/leads
curl "https://api.liveleg.dev/v1/lead_lists/lst_roofing_tx/leads?status=scheduled&limit=50" \
-H "Authorization: Bearer $LIVELEG_API_KEY"const res = await fetch('https://api.liveleg.dev/v1/lead_lists/lst_roofing_tx/leads?status=scheduled&limit=50', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
},
});
const data = await res.json();import os, requests
data = requests.get(
"https://api.liveleg.dev/v1/lead_lists/lst_roofing_tx/leads",
headers={"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}"},
params={
"status": "scheduled",
"limit": 50,
},
).json() {
"id": "led_44Hc2",
"object": "lead",
"lead_list_id": "lst_roofing_tx",
"campaign_id": "cmp_roofing_q4",
"phone": "+15125550147",
"state": "TX",
"name": "Dana Ruiz",
"company": "Ridgeline Roofing",
"external_id": "crm_88213",
"status": "scheduled",
"status_reason": "retry_delay",
"attempts": 2,
"caller_id": "+15125550199",
"caller_id_since": "2026-11-02T21:04:18Z",
"last_call_id": "cal_7Hd1aP2c",
"last_called_at": "2026-11-03T15:02:11Z",
"next_call_at": "2026-11-03T19:02:11Z",
"created_at": "2026-11-02T20:15:30Z"
} | Status | Meaning |
|---|---|
queued | Waiting for its turn in list order. |
scheduled | Has a next_call_at in the future. status_reason is callback, retry_delay, outside_calling_hours or state_call_limit. |
in_call | A line is ringing or the lead is talking with an agent. |
completed | Reached a person and the call has a disposition with no callback time. |
exhausted | Reached max_attempts without a person answering. |
held | Not dialed until you change the lead or the data behind the block. status_reason is state_unknown, no_consent, national_dnc or state_dnc. Fix it with PATCH and the lead returns to queued. |
do_not_call | The number is on your do-not-call list or opted out. status_reason is internal_dnc. |
invalid | Number lookup found the line disconnected. status_reason is line_disconnected. |
caller_id is the number this lead sees on every call, and caller_id_since is when it was first used. See Caller ID. State call limits count calls from all your numbers. When no lead in a list can be dialed now, you get lead_list.emptied with the counts by status.
Start a session
One request per agent. The agent must be connected in the browser or on a SIP phone before dialing starts; see Agent calling.
POST/v1/dial_sessions
| Parameters | Type | Description |
|---|---|---|
agent_idrequired | string | The agent who takes the calls. Use agent_pool_id instead to share one session across a pool of agents. |
agent_pool_id | string | Agents in the pool share answered calls. A second live person goes to the next free agent. |
lead_list_idrequired | string | The list to dial. Leads are dialed in list order, with scheduled callbacks first. |
lines | auto | 1-5 | Lines per agent. auto sets the count from the live answer rate and the campaign abandonment rate. Default auto. |
max_lines | integer | Upper limit when lines is auto. 1 to 5, default 5. |
caller_id_pool | string | The pool caller IDs are picked from. See caller ID selection. |
campaign_id | string | The campaign the session counts toward for the 30-day abandonment rate, calling hours and attempts. Defaults to the lead list's campaign. |
amd | boolean | Answering machine detection. Default true. |
recording | smart | off | Smart Recording. Default smart. |
transcription | off | post_call | live | Transcripts for bridged calls. Default off. |
metadata | object | Up to 20 keys. Returned on every event for the session. |
curl https://api.liveleg.dev/v1/dial_sessions \
-H "Authorization: Bearer $LIVELEG_API_KEY" \
-H "Idempotency-Key: agent-114-morning" \
-d agent_id=agt_114 \
-d lead_list_id=lst_roofing_tx \
-d lines=auto \
-d caller_id_pool=pool_local_presence \
-d amd=true \
-d recording=smartconst res = await fetch('https://api.liveleg.dev/v1/dial_sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIVELEG_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'agent-114-morning',
},
body: JSON.stringify({
agent_id: 'agt_114',
lead_list_id: 'lst_roofing_tx',
lines: 'auto',
caller_id_pool: 'pool_local_presence',
amd: true,
recording: 'smart',
}),
});
const session = await res.json();import os, requests
session = requests.post(
"https://api.liveleg.dev/v1/dial_sessions",
headers={
"Authorization": f"Bearer {os.environ['LIVELEG_API_KEY']}",
"Idempotency-Key": "agent-114-morning",
},
json={
"agent_id": "agt_114",
"lead_list_id": "lst_roofing_tx",
"lines": "auto",
"caller_id_pool": "pool_local_presence",
"amd": True,
"recording": "smart",
},
).json() {
"id": "dls_31Ka9",
"object": "dial_session",
"status": "running",
"livemode": true,
"agent_id": "agt_114",
"lead_list_id": "lst_roofing_tx",
"campaign_id": "cmp_roofing_q4",
"lines": "auto",
"max_lines": 5,
"current_lines": 3,
"caller_id_pool": "pool_local_presence",
"amd": true,
"recording": "smart",
"totals": {"dialed": 0, "answered": 0, "bridged": 0, "blocked": 0},
"created_at": "2026-11-03T14:00:02.118Z"
} Pause, resume and stop
Pausing lets the lines that are already ringing finish and places no new ones. The agent stays connected. Stopping ends the session and returns the totals.
curl -X POST https://api.liveleg.dev/v1/dial_sessions/dls_31Ka9/pause \
-H "Authorization: Bearer $LIVELEG_API_KEY"
curl -X POST https://api.liveleg.dev/v1/dial_sessions/dls_31Ka9/resume \
-H "Authorization: Bearer $LIVELEG_API_KEY"
curl -X POST https://api.liveleg.dev/v1/dial_sessions/dls_31Ka9/stop \
-H "Authorization: Bearer $LIVELEG_API_KEY" A session also stops on its own when the agent has been idle for 15 minutes, the list is empty, or calling hours have closed for every lead left. The reason is on the dial_session.stopped event.
Events
A session sends dial_session.* events for its own state, including dial_session.lines_changed when auto lines go up or down. It sends leg.* events for each line, call.bridged when a person is connected and lead.status_changed as leads move through the list. The full list is in Webhooks and events.
{
"id": "evt_2Wc81Lk0",
"type": "call.bridged",
"created_at": "2026-11-03T15:42:07.412Z",
"livemode": true,
"data": {
"call_id": "cal_8Jq2mT4x",
"dial_session_id": "dls_31Ka9",
"agent_id": "agt_114",
"lead_id": "led_44Hc2",
"to": "+15125550147",
"from": "+15125550199",
"amd_result": "human",
"speech_ended_to_bridge_ms": 980,
"legs_canceled": 2
}
}