English
English
Appearance
English
English
Appearance
This group covers outbound-campaign administration: creation, lifecycle control (launch/pause/resume/archive/clone), performance and time-series statistics, and managing the agents assigned to each campaign. The per-call lifecycle (initiate / status / hangup / disposition) is documented at the end of the page for partner integrations.
POST /api/telesales/campaigns| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
name | string | ✅ | Campaign name, unique within the account | max 150 chars |
description | string | optional | Short description | max 500 chars |
dialer_mode | string | ✅ | Dialer mode | manual / preview / progressive / predictive |
caller_id_group_id | integer | ✅ | Caller-ID rotation group ID (see §Caller-ID rotation groups) | Must exist + status=active |
script_id | integer | optional | ID of the call script assigned to the campaign | Must exist |
ring_timeout_sec | integer | optional | Maximum ring time (seconds) | 5-120 (default 25) |
preview_window_sec | integer | optional | Time the agent has to preview the lead when dialer_mode=preview | 0-60 (default 10) |
priority | integer | optional | Priority — higher number = higher priority | 0-1000 (default 100) |
max_attempts | integer | optional | Maximum retry attempts per lead | 1-20 (default 5) |
pdpl_enforce | bool | optional | Enforce legal calling window (PDPL/TCPA) | true / false (default true) |
abandon_rate_limit_pct | float | optional | Maximum abandon-rate threshold (% — predictive mode) | 0.0-10.0 (default 3.0) |
time_window | object | optional | Allowed calling windows per day of week | Key: mon/tue/wed/thu/fri/sat/sun → array of {from,to} HH:MM |
dialer_mode values:
manual — the agent dials each number themselves.preview — the system pushes the lead to the agent for preview, then dials.progressive — the system dials when an agent is free.predictive — the system dials before an agent is free (constrained by abandon_rate_limit_pct).Body:
{
"name": "Outbound Sales Q3-2026",
"description": "Q3 outbound campaign",
"dialer_mode": "progressive",
"caller_id_group_id": 3,
"script_id": 12,
"ring_timeout_sec": 25,
"preview_window_sec": 10,
"priority": 100,
"max_attempts": 5,
"pdpl_enforce": true,
"abandon_rate_limit_pct": 3.0,
"time_window": {
"mon": [{"from": "08:00", "to": "17:30"}],
"tue": [{"from": "08:00", "to": "17:30"}],
"wed": [{"from": "08:00", "to": "17:30"}],
"thu": [{"from": "08:00", "to": "17:30"}],
"fri": [{"from": "08:00", "to": "17:30"}],
"sat": [{"from": "08:00", "to": "12:00"}]
}
}| Field | Type | Description | Valid values |
|---|---|---|---|
id | integer | Campaign ID | |
name | string | Campaign name | |
description | string|null | Short description | |
status | string | Lifecycle status | draft / active / paused / archived |
dialer_mode | string | Dialer mode | manual / preview / progressive / predictive |
caller_id_group_id | integer | Caller-ID rotation group ID | |
script_id | integer|null | Script ID | |
ring_timeout_sec | integer | Maximum ring time | 5-120 |
preview_window_sec | integer | Preview window | 0-60 |
priority | integer | Priority | 0-1000 |
max_attempts | integer | Maximum retry attempts | 1-20 |
pdpl_enforce | bool | Enforce PDPL window | |
abandon_rate_limit_pct | float | Abandon-rate threshold (predictive) | 0.0-10.0 |
time_window | object | Calling window per day | |
total_leads | integer | Number of leads imported into the campaign | |
created_by | integer | User ID of the creator | |
created_at | datetime | Creation timestamp (ISO 8601 UTC) | |
updated_at | datetime | Last update timestamp |
Response 201:
{
"data": {
"id": 36,
"name": "Outbound Sales Q3-2026",
"description": "Q3 outbound campaign",
"status": "draft",
"dialer_mode": "progressive",
"caller_id_group_id": 3,
"script_id": 12,
"ring_timeout_sec": 25,
"preview_window_sec": 10,
"priority": 100,
"max_attempts": 5,
"pdpl_enforce": true,
"abandon_rate_limit_pct": 3.0,
"time_window": { "mon": [{"from":"08:00","to":"17:30"}], "...": "..." },
"total_leads": 0,
"created_by": 7,
"created_at": "2026-06-06T06:00:00Z",
"updated_at": "2026-06-06T06:00:00Z"
}
}Response 422 (validation failed):
{
"message": "The given data was invalid.",
"errors": {
"dialer_mode": ["The selected dialer mode is invalid."],
"caller_id_group_id": ["The selected caller id group id is invalid."]
}
}| Endpoint | Action | Response |
|---|---|---|
POST /api/telesales/campaigns/{id}/launch | draft → active | { "data": { "id":36, "status":"active", "launched_at":"...Z" } } |
POST /api/telesales/campaigns/{id}/pause | active → paused | { "data": { "id":36, "status":"paused" } } |
POST /api/telesales/campaigns/{id}/resume | paused → active | { "data": { "id":36, "status":"active" } } |
POST /api/telesales/campaigns/{id}/archive | → archived | { "data": { "id":36, "status":"archived" } } |
POST /api/telesales/campaigns/{id}/clone | → new draft | { "data": { "id":42, "status":"draft", "name":"... (clone)" } } |
PUT /api/telesales/campaigns/{id} | Update fields | returns the full campaign object |
DELETE /api/telesales/campaigns/{id} | Soft delete | 204 No Content |
GET /api/telesales/campaigns — list | Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
status | string | optional | Filter by lifecycle status | draft / active / paused / archived |
dialer_mode | string | optional | Filter by dialer mode | manual / preview / progressive / predictive |
q | string | optional | Search by name (LIKE %keyword%) | |
page | integer | optional | Page number | ≥ 1 (default 1) |
per_page | integer | optional | Records per page | 1-200 (default 50) |
data[]) | Field | Type | Description | Valid values |
|---|---|---|---|
id | integer | Campaign ID | |
name | string | Campaign name | |
status | string | Lifecycle status | draft / active / paused / archived |
dialer_mode | string | Dialer mode | manual / preview / progressive / predictive |
total_leads | integer | Total leads imported | |
dialled | integer | Total dial attempts | |
answered | integer | Number of answered calls | |
converted | integer | Number of successful conversions | |
asr_pct | float | Answer-Seizure Ratio (%) = answered/dialled*100 | 0-100 |
Response 200:
{
"data": [
{ "id": 36, "name": "Outbound Sales Q3-2026", "status": "active",
"dialer_mode": "progressive", "total_leads": 487, "dialled": 312,
"answered": 198, "converted": 47, "asr_pct": 63.5 }
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
}GET /api/telesales/campaigns/{id}/stats — aggregate statistics | Field | Type | Description | Valid values |
|---|---|---|---|
dialled | integer | Total dial attempts | |
answered | integer | Total answered calls | |
talked_over_30s | integer | Calls with billsec ≥ 30 | |
converted | integer | Converted calls (disposition is_final=true + successful outcome) | |
answer_rate_pct | float | answered/dialled*100 | 0-100 |
conversion_rate_pct | float | converted/answered*100 | 0-100 |
asr_pct | float | Answer-Seizure Ratio (%) | 0-100 |
aht_seconds | integer | Average Handle Time (seconds) — average per-call handling time | |
abandon_rate_pct | float | Abandon rate (% — predictive mode only) | 0-100 |
callbacks_pending | integer | Number of callbacks pending |
Response 200:
{
"data": {
"dialled": 312,
"answered": 198,
"talked_over_30s": 142,
"converted": 47,
"answer_rate_pct": 63.46,
"conversion_rate_pct": 15.06,
"asr_pct": 63.46,
"aht_seconds": 184,
"abandon_rate_pct": 1.8,
"callbacks_pending": 23
}
}GET /api/telesales/campaigns/{id}/timeseries — time series | Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
group_by | string | optional | Grouping unit | hour / day / week (default day) |
date_from | datetime | optional | Start timestamp (UTC ISO 8601) | |
date_to | datetime | optional | End timestamp (UTC ISO 8601) |
data[]) | Field | Type | Description | Valid values |
|---|---|---|---|
bucket | string | Time bucket — formatted by group_by | YYYY-MM-DD / YYYY-Www / YYYY-MM-DD HH:00:00 |
dialled | integer | Dial attempts in this bucket | |
answered | integer | Answered calls in this bucket | |
converted | integer | Converted calls in this bucket |
Response 200:
{
"data": [
{ "bucket": "2026-06-01", "dialled": 80, "answered": 51, "converted": 12 },
{ "bucket": "2026-06-02", "dialled": 92, "answered": 60, "converted": 15 },
{ "bucket": "2026-06-03", "dialled": 75, "answered": 47, "converted": 9 }
],
"meta": { "range": "7d" }
}| Endpoint | Purpose |
|---|---|
GET /api/telesales/campaigns/{id}/agents | List the agents currently assigned to the campaign |
GET /api/telesales/campaigns/{id}/agents/available | List users eligible to be assigned (not conflicting with another campaign, appropriate role) |
POST /api/telesales/campaigns/{id}/agents | Assign an agent to the campaign |
PATCH /api/telesales/campaigns/{id}/agents/{userId} | Update an agent's role or status |
DELETE /api/telesales/campaigns/{id}/agents/{userId} | Remove an agent from the campaign |
GET /api/telesales/agents — list the customer's agents Integrators use this to pick a user_id for POST /api/telesales/campaigns/{id}/agents:
GET /api/telesales/agents?per_page=50&page=1| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
q | string | optional | Search by name / username / email (LIKE %keyword%) | |
role | string | optional | Filter by role | agent / supervisor / manager / ... |
team_id | integer | optional | Filter by team | |
page | integer | optional | Page number | ≥ 1 (default 1) |
per_page | integer | optional | Records per page | 1-200 (default 50) |
data[]) | Field | Type | Description | Valid values |
|---|---|---|---|
id | integer | User ID | |
name | string | Full name | |
username | string | Login name | |
email | string | ||
role | string | System role | agent / supervisor / manager |
team_id | integer|null | Team ID the user belongs to | |
extension | string|null | Assigned SIP extension | |
is_active | bool | Whether the user is still active | true / false |
Response 200:
{
"data": [
{
"id": 25,
"name": "Nguyen Van A",
"username": "nguyenvana",
"email": "nguyenvana@company.com",
"role": "agent",
"team_id": 3,
"extension": "1001",
"is_active": true
}
],
"meta": { "current_page": 1, "last_page": 3, "per_page": 50, "total": 142 }
}Only returns users with is_active=true, sorted by name.
Workflow to assign an agent to a campaign:
# Step 1: list available agents
GET /api/telesales/agents?role=agent
# Step 2: assign user_id=25 to the campaign
POST /api/telesales/campaigns/36/agents
{ "user_id": 25, "role": "agent" }GET /api/telesales/campaigns/{id}/agents data[]) | Field | Type | Description | Valid values |
|---|---|---|---|
user_id | integer | Agent user ID | |
username | string | Login name | |
display_name | string | Display name | |
campaign_id | integer | Campaign ID | |
role | string | Role in the campaign | agent / supervisor |
extension | string|null | SIP extension | |
team_id | integer|null | Team ID | |
team_name | string|null | Team name | |
is_active | bool | Whether the user is still active | true / false |
assigned_at | datetime | When the agent was assigned (ISO 8601 UTC) |
Response 200:
{
"data": [
{
"user_id": 25,
"username": "agent01",
"display_name": "Nguyen Van A",
"campaign_id": 36,
"role": "agent",
"extension": "2001",
"team_id": 3,
"team_name": "Sales A",
"is_active": true,
"assigned_at": "2026-06-06T06:10:00Z"
}
]
}GET /api/telesales/campaigns/{id}/agents/available data[]) | Field | Type | Description | Valid values |
|---|---|---|---|
user_id | integer | User ID eligible for assignment | |
username | string | Login name | |
display_name | string | Display name | |
extension | string|null | SIP extension | |
team_id | integer|null | Team ID | |
team_name | string|null | Team name | |
role | string | System role | agent / supervisor |
is_active | bool | Whether the user is still active | true / false |
Response 200:
{
"data": [
{
"user_id": 30,
"username": "agent06",
"display_name": "Tran Thi B",
"extension": "2006",
"team_id": 3,
"team_name": "Sales A",
"role": "agent",
"is_active": true
}
]
}POST /api/telesales/campaigns/{id}/agents — assign an agent | Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
user_id | integer | ✅ | User ID to assign | Must exist + is_active=true + have a SIP extension |
role | string | optional | Role in the campaign | agent (default) / supervisor |
Body:
{ "user_id": 25, "role": "agent" }| Field | Type | Description | Valid values |
|---|---|---|---|
user_id | integer | The user ID just assigned | |
campaign_id | integer | Campaign ID | |
role | string | Role in the campaign | agent / supervisor |
assigned_at | datetime | When the assignment happened (ISO 8601 UTC) |
Response 201:
{
"data": {
"user_id": 25,
"campaign_id": 36,
"role": "agent",
"assigned_at": "2026-06-06T06:10:00Z"
}
}Response 422 — validation / conflict cases:
// User does not exist or is inactive
{
"message": "The given data was invalid.",
"errors": { "user_id": ["The selected user id is invalid."] }
}
// User is already assigned to another campaign (a user can only belong to one campaign at a time)
{
"message": "Agent is already attached to another active campaign.",
"errors": { "user_id": ["User 25 already assigned to campaign 38."] }
}
// User has no SIP extension (cannot receive calls)
{
"message": "User has no SIP extension assigned — cannot be assigned as an agent.",
"errors": { "user_id": ["User 25 has no extension assigned."] }
}
// Invalid role (only agent / supervisor are accepted)
{
"message": "The given data was invalid.",
"errors": { "role": ["The selected role is invalid."] }
}PATCH /api/telesales/campaigns/{id}/agents/{userId} — update role | Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
role | string | ✅ | New role in the campaign | agent / supervisor |
Body:
{ "role": "supervisor" }Response 200:
{ "data": { "user_id": 25, "campaign_id": 36, "role": "supervisor", "assigned_at": "2026-06-06T06:10:00Z" } }Response 404 — the agent is not assigned to this campaign:
{ "message": "Agent is not attached to this campaign." }DELETE /api/telesales/campaigns/{id}/agents/{userId} Response 204 No Content (success).
Response 422 — agent is currently on a call:
{
"message": "Cannot unassign an agent who is currently on a call.",
"errors": { "user_id": ["Agent 25 is currently on call UUID xxx — wait for it to end or force-hangup first."] }
}Note
After a successful DELETE, the agent will no longer receive new calls from the campaign, but any in-flight call will run to completion. Realtime presence state syncs within 5 seconds.
Each call goes through four main steps initiated by the agent (or the system) within a campaign.
Tip
If you use the Webphone SDK, all four steps are bundled into phone.call(number, options) — you do not need to call the endpoints below directly. See the Webphone SDK docs.
POST /api/telesales/calls/initiateThe agent's extension is taken from the authenticated user's profile — no need to pass it manually.
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
campaign_id | integer | ✅ | ID of the campaign the agent is running | Must exist + status=active + agent assigned |
lead_id | integer | ✅ | ID of the lead to dial | Must belong to campaign_id + status not in dnc/exhausted/converted |
Body:
{
"campaign_id": 36,
"lead_id": 78912
}| Field | Type | Description | Valid values |
|---|---|---|---|
uuid | string | Call UUID — used by every sub-endpoint (/status, /hangup, /disposition) | UUID v4 |
status | string | Initial state | originating |
caller_id_used | string | Outbound caller ID picked by the rotation group | E.164 |
lead_id | integer | Lead ID | |
campaign_id | integer | Campaign ID |
Response 200:
{
"uuid": "093e1024-82c6-49b3-8775-99edeb221898",
"status": "originating",
"caller_id_used": "0900000010",
"lead_id": 78912,
"campaign_id": 36
}Response 422 (PDPL window violation):
{
"error": "pdpl_blocked",
"message": "Outside the campaign calling window (08:00-17:30, Mon-Sat).",
"next_eligible_at": "2026-06-07T01:00:00Z"
}Response 422 (other reasons):
{
"error": "lead_dnc",
"message": "Lead phone is on the DNC list."
}Other error codes: lead_blocked, no_caller_id_available, max_attempts_reached.
GET /api/telesales/calls/{uuid}/status| Field | Type | Description | Valid values |
|---|---|---|---|
uuid | string | Call UUID | UUID v4 |
state | string | Current state | originating / ringing / answered / wrap_up / closed |
start_time | datetime | When the call was initiated (ISO 8601 UTC) | |
answer_time | datetime|null | When the customer answered | null if not yet answered |
end_time | datetime|null | When the call ended | null if still running |
duration | integer|null | Total time from originate to hangup (seconds) | |
billsec | integer|null | Talk time (seconds) — from answer_time to end_time | |
result | string|null | Final result — derived from the hangup cause | answered / no_answer / busy / failed / cancelled |
hangup_cause | string|null | Hangup reason from Zorio PBX | e.g. NORMAL_CLEARING / USER_BUSY |
caller_id_used | string | Outbound number used | E.164 |
agent_extension | string | Agent's SIP extension | |
destination_number | string | Destination phone (lead) |
POST /api/telesales/calls/{uuid}/hangup| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
cause | string | optional | Custom hangup reason — stored in CDR for reporting | free text, max 64 chars (default MANUAL_HANGUP) |
Body (optional):
{ "cause": "MANUAL_HANGUP" }| Field | Type | Description | Valid values |
|---|---|---|---|
uuid | string | Call UUID | UUID v4 |
state | string | State after hangup | wrap_up (waiting for the agent to dispose the call) |
end_time | datetime | When hangup happened (ISO 8601 UTC) | |
duration | integer | Total call duration (seconds) | |
billsec | integer | Talk time (seconds) | |
hangup_cause | string | Hangup cause (echoed from request or auto-generated) |
Response 200:
{
"data": {
"uuid": "093e1024-82c6-49b3-8775-99edeb221898",
"state": "wrap_up",
"end_time": "2026-06-06T06:35:42Z",
"duration": 222,
"billsec": 213,
"hangup_cause": "MANUAL_HANGUP"
}
}Response 404 — UUID does not exist or does not belong to the account:
{ "message": "Call not found." }Response 422 — the call has already ended:
{
"message": "Call has already ended, hangup is not possible.",
"errors": { "uuid": ["Call already in state 'closed' since 2026-06-06T06:34:00Z."] }
}Response 403 — an ordinary agent tries to hang up someone else's call without supervisor privilege:
{ "message": "You are not allowed to hang up this call. Supervisor role is required to force hangup." }POST /api/telesales/calls/{uuid}/disposition| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
disposition_id | integer | ✅ | Disposition ID (mapped from code, e.g. SALE-OK) | Must exist + is_active=true |
note | string | optional | Agent's free-text note | max 1000 chars |
callback_at | datetime | optional | Callback schedule (ISO 8601 UTC) — only when the disposition has trigger_callback=true | Must be in the future |
callback_agent_id | integer | optional | Sticky agent for the callback | User ID; must be an agent of the campaign |
Body:
{
"disposition_id": 5,
"note": "Customer committed, follow-up email scheduled",
"callback_at": null,
"callback_agent_id": null
}| Field | Type | Description | Valid values |
|---|---|---|---|
uuid | string | Call UUID | UUID v4 |
state | string | State after disposition | closed |
disposition | object | Details of the applied disposition | {id, code, label, category} |
disposition.id | integer | Disposition ID | |
disposition.code | string | Immutable code (e.g. SALE-OK) | |
disposition.label | string | Display label | |
disposition.category | string | Category | contact / no_contact / callback / remove |
lead_id | integer | Lead ID | |
lead_status_after | string | Lead status after applying the disposition | pending / contacted / converted / dnc / exhausted |
callback_scheduled | object|null | Callback info (if any) | {callback_at, agent_id} or null |
Response 200:
{
"data": {
"uuid": "093e1024-82c6-49b3-8775-99edeb221898",
"state": "closed",
"disposition": { "id": 5, "code": "SALE-OK", "label": "Sale Success", "category": "contact" },
"lead_id": 78912,
"lead_status_after": "converted",
"callback_scheduled": null
}
}Industry-specific outcome fields
Industry-specific outcome fields (Payment Method, Appointment Date, Survey Result, Lead Score, Notes, ...) are declared as custom fields on each disposition, configured by the customer's admin. Detailed schemas per industry (customer care, service appointments, satisfaction surveys, ...) are shipped in a separate per-customer configuration package.