Skip to content

Campaigns

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.

3.1 Create a campaign

http
POST /api/telesales/campaigns

Request body fields

FieldTypeRequiredDescriptionValid values
namestringCampaign name, unique within the accountmax 150 chars
descriptionstringoptionalShort descriptionmax 500 chars
dialer_modestringDialer modemanual / preview / progressive / predictive
caller_id_group_idintegerCaller-ID rotation group ID (see §Caller-ID rotation groups)Must exist + status=active
script_idintegeroptionalID of the call script assigned to the campaignMust exist
ring_timeout_secintegeroptionalMaximum ring time (seconds)5-120 (default 25)
preview_window_secintegeroptionalTime the agent has to preview the lead when dialer_mode=preview0-60 (default 10)
priorityintegeroptionalPriority — higher number = higher priority0-1000 (default 100)
max_attemptsintegeroptionalMaximum retry attempts per lead1-20 (default 5)
pdpl_enforcebooloptionalEnforce legal calling window (PDPL/TCPA)true / false (default true)
abandon_rate_limit_pctfloatoptionalMaximum abandon-rate threshold (% — predictive mode)0.0-10.0 (default 3.0)
time_windowobjectoptionalAllowed calling windows per day of weekKey: 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:

json
{
  "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"}]
  }
}

Response body fields

FieldTypeDescriptionValid values
idintegerCampaign ID
namestringCampaign name
descriptionstring|nullShort description
statusstringLifecycle statusdraft / active / paused / archived
dialer_modestringDialer modemanual / preview / progressive / predictive
caller_id_group_idintegerCaller-ID rotation group ID
script_idinteger|nullScript ID
ring_timeout_secintegerMaximum ring time5-120
preview_window_secintegerPreview window0-60
priorityintegerPriority0-1000
max_attemptsintegerMaximum retry attempts1-20
pdpl_enforceboolEnforce PDPL window
abandon_rate_limit_pctfloatAbandon-rate threshold (predictive)0.0-10.0
time_windowobjectCalling window per day
total_leadsintegerNumber of leads imported into the campaign
created_byintegerUser ID of the creator
created_atdatetimeCreation timestamp (ISO 8601 UTC)
updated_atdatetimeLast update timestamp

Response 201:

json
{
  "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):

json
{
  "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."]
  }
}

3.2 Campaign lifecycle control

EndpointActionResponse
POST /api/telesales/campaigns/{id}/launchdraft → active{ "data": { "id":36, "status":"active", "launched_at":"...Z" } }
POST /api/telesales/campaigns/{id}/pauseactive → paused{ "data": { "id":36, "status":"paused" } }
POST /api/telesales/campaigns/{id}/resumepaused → 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 fieldsreturns the full campaign object
DELETE /api/telesales/campaigns/{id}Soft delete204 No Content

3.3 List / detail / statistics

GET /api/telesales/campaigns — list

Query parameters

FieldTypeRequiredDescriptionValid values
statusstringoptionalFilter by lifecycle statusdraft / active / paused / archived
dialer_modestringoptionalFilter by dialer modemanual / preview / progressive / predictive
qstringoptionalSearch by name (LIKE %keyword%)
pageintegeroptionalPage number≥ 1 (default 1)
per_pageintegeroptionalRecords per page1-200 (default 50)

Response fields (each item in data[])

FieldTypeDescriptionValid values
idintegerCampaign ID
namestringCampaign name
statusstringLifecycle statusdraft / active / paused / archived
dialer_modestringDialer modemanual / preview / progressive / predictive
total_leadsintegerTotal leads imported
dialledintegerTotal dial attempts
answeredintegerNumber of answered calls
convertedintegerNumber of successful conversions
asr_pctfloatAnswer-Seizure Ratio (%) = answered/dialled*1000-100

Response 200:

json
{
  "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

Response body fields

FieldTypeDescriptionValid values
dialledintegerTotal dial attempts
answeredintegerTotal answered calls
talked_over_30sintegerCalls with billsec ≥ 30
convertedintegerConverted calls (disposition is_final=true + successful outcome)
answer_rate_pctfloatanswered/dialled*1000-100
conversion_rate_pctfloatconverted/answered*1000-100
asr_pctfloatAnswer-Seizure Ratio (%)0-100
aht_secondsintegerAverage Handle Time (seconds) — average per-call handling time
abandon_rate_pctfloatAbandon rate (% — predictive mode only)0-100
callbacks_pendingintegerNumber of callbacks pending

Response 200:

json
{
  "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

Query parameters

FieldTypeRequiredDescriptionValid values
group_bystringoptionalGrouping unithour / day / week (default day)
date_fromdatetimeoptionalStart timestamp (UTC ISO 8601)
date_todatetimeoptionalEnd timestamp (UTC ISO 8601)

Response fields (each item in data[])

FieldTypeDescriptionValid values
bucketstringTime bucket — formatted by group_byYYYY-MM-DD / YYYY-Www / YYYY-MM-DD HH:00:00
dialledintegerDial attempts in this bucket
answeredintegerAnswered calls in this bucket
convertedintegerConverted calls in this bucket

Response 200:

json
{
  "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" }
}

3.4 Managing agents in a campaign

EndpointPurpose
GET /api/telesales/campaigns/{id}/agentsList the agents currently assigned to the campaign
GET /api/telesales/campaigns/{id}/agents/availableList users eligible to be assigned (not conflicting with another campaign, appropriate role)
POST /api/telesales/campaigns/{id}/agentsAssign 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:

http
GET /api/telesales/agents?per_page=50&page=1

Query parameters

FieldTypeRequiredDescriptionValid values
qstringoptionalSearch by name / username / email (LIKE %keyword%)
rolestringoptionalFilter by roleagent / supervisor / manager / ...
team_idintegeroptionalFilter by team
pageintegeroptionalPage number≥ 1 (default 1)
per_pageintegeroptionalRecords per page1-200 (default 50)

Response fields (each item in data[])

FieldTypeDescriptionValid values
idintegerUser ID
namestringFull name
usernamestringLogin name
emailstringEmail
rolestringSystem roleagent / supervisor / manager
team_idinteger|nullTeam ID the user belongs to
extensionstring|nullAssigned SIP extension
is_activeboolWhether the user is still activetrue / false

Response 200:

json
{
  "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:

http
# 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

Response fields (each item in data[])

FieldTypeDescriptionValid values
user_idintegerAgent user ID
usernamestringLogin name
display_namestringDisplay name
campaign_idintegerCampaign ID
rolestringRole in the campaignagent / supervisor
extensionstring|nullSIP extension
team_idinteger|nullTeam ID
team_namestring|nullTeam name
is_activeboolWhether the user is still activetrue / false
assigned_atdatetimeWhen the agent was assigned (ISO 8601 UTC)

Response 200:

json
{
  "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

Response fields (each item in data[])

FieldTypeDescriptionValid values
user_idintegerUser ID eligible for assignment
usernamestringLogin name
display_namestringDisplay name
extensionstring|nullSIP extension
team_idinteger|nullTeam ID
team_namestring|nullTeam name
rolestringSystem roleagent / supervisor
is_activeboolWhether the user is still activetrue / false

Response 200:

json
{
  "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

Request body fields

FieldTypeRequiredDescriptionValid values
user_idintegerUser ID to assignMust exist + is_active=true + have a SIP extension
rolestringoptionalRole in the campaignagent (default) / supervisor

Body:

json
{ "user_id": 25, "role": "agent" }

Response body fields

FieldTypeDescriptionValid values
user_idintegerThe user ID just assigned
campaign_idintegerCampaign ID
rolestringRole in the campaignagent / supervisor
assigned_atdatetimeWhen the assignment happened (ISO 8601 UTC)

Response 201:

json
{
  "data": {
    "user_id": 25,
    "campaign_id": 36,
    "role": "agent",
    "assigned_at": "2026-06-06T06:10:00Z"
  }
}

Response 422 — validation / conflict cases:

json
// 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

Request body fields

FieldTypeRequiredDescriptionValid values
rolestringNew role in the campaignagent / supervisor

Body:

json
{ "role": "supervisor" }

Response 200:

json
{ "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:

json
{ "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:

json
{
  "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.


7. Call Lifecycle

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.

7.1 Initiate a call

http
POST /api/telesales/calls/initiate

The agent's extension is taken from the authenticated user's profile — no need to pass it manually.

Request body fields

FieldTypeRequiredDescriptionValid values
campaign_idintegerID of the campaign the agent is runningMust exist + status=active + agent assigned
lead_idintegerID of the lead to dialMust belong to campaign_id + status not in dnc/exhausted/converted

Body:

json
{
  "campaign_id": 36,
  "lead_id": 78912
}

Response body fields

FieldTypeDescriptionValid values
uuidstringCall UUID — used by every sub-endpoint (/status, /hangup, /disposition)UUID v4
statusstringInitial stateoriginating
caller_id_usedstringOutbound caller ID picked by the rotation groupE.164
lead_idintegerLead ID
campaign_idintegerCampaign ID

Response 200:

json
{
  "uuid": "093e1024-82c6-49b3-8775-99edeb221898",
  "status": "originating",
  "caller_id_used": "0900000010",
  "lead_id": 78912,
  "campaign_id": 36
}

Response 422 (PDPL window violation):

json
{
  "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):

json
{
  "error": "lead_dnc",
  "message": "Lead phone is on the DNC list."
}

Other error codes: lead_blocked, no_caller_id_available, max_attempts_reached.

7.2 Track call status

http
GET /api/telesales/calls/{uuid}/status

Response body fields

FieldTypeDescriptionValid values
uuidstringCall UUIDUUID v4
statestringCurrent stateoriginating / ringing / answered / wrap_up / closed
start_timedatetimeWhen the call was initiated (ISO 8601 UTC)
answer_timedatetime|nullWhen the customer answerednull if not yet answered
end_timedatetime|nullWhen the call endednull if still running
durationinteger|nullTotal time from originate to hangup (seconds)
billsecinteger|nullTalk time (seconds) — from answer_time to end_time
resultstring|nullFinal result — derived from the hangup causeanswered / no_answer / busy / failed / cancelled
hangup_causestring|nullHangup reason from Zorio PBXe.g. NORMAL_CLEARING / USER_BUSY
caller_id_usedstringOutbound number usedE.164
agent_extensionstringAgent's SIP extension
destination_numberstringDestination phone (lead)

7.3 Manual hangup

http
POST /api/telesales/calls/{uuid}/hangup

Request body fields

FieldTypeRequiredDescriptionValid values
causestringoptionalCustom hangup reason — stored in CDR for reportingfree text, max 64 chars (default MANUAL_HANGUP)

Body (optional):

json
{ "cause": "MANUAL_HANGUP" }

Response body fields

FieldTypeDescriptionValid values
uuidstringCall UUIDUUID v4
statestringState after hangupwrap_up (waiting for the agent to dispose the call)
end_timedatetimeWhen hangup happened (ISO 8601 UTC)
durationintegerTotal call duration (seconds)
billsecintegerTalk time (seconds)
hangup_causestringHangup cause (echoed from request or auto-generated)

Response 200:

json
{
  "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:

json
{ "message": "Call not found." }

Response 422 — the call has already ended:

json
{
  "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:

json
{ "message": "You are not allowed to hang up this call. Supervisor role is required to force hangup." }

7.4 Submit disposition

http
POST /api/telesales/calls/{uuid}/disposition

Request body fields

FieldTypeRequiredDescriptionValid values
disposition_idintegerDisposition ID (mapped from code, e.g. SALE-OK)Must exist + is_active=true
notestringoptionalAgent's free-text notemax 1000 chars
callback_atdatetimeoptionalCallback schedule (ISO 8601 UTC) — only when the disposition has trigger_callback=trueMust be in the future
callback_agent_idintegeroptionalSticky agent for the callbackUser ID; must be an agent of the campaign

Body:

json
{
  "disposition_id": 5,
  "note": "Customer committed, follow-up email scheduled",
  "callback_at": null,
  "callback_agent_id": null
}

Response body fields

FieldTypeDescriptionValid values
uuidstringCall UUIDUUID v4
statestringState after dispositionclosed
dispositionobjectDetails of the applied disposition{id, code, label, category}
disposition.idintegerDisposition ID
disposition.codestringImmutable code (e.g. SALE-OK)
disposition.labelstringDisplay label
disposition.categorystringCategorycontact / no_contact / callback / remove
lead_idintegerLead ID
lead_status_afterstringLead status after applying the dispositionpending / contacted / converted / dnc / exhausted
callback_scheduledobject|nullCallback info (if any){callback_at, agent_id} or null

Response 200:

json
{
  "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.

Cấp phép theo điều khoản sử dụng của Zorio.