Skip to content

Reports

A group of 9 reports for the Telesales module: dashboard overview, disposition breakdown, agent performance, funnel, caller-ID health, hour x day-of-week heatmap, lead-source attribution, time series and abandon rate. All support CSV/Excel export.

Common query params

Every endpoint shares the following parameters (UTC ISO 8601):

FieldTypeRequiredDescriptionValid values
campaign_idintegeroptionalFilter by campaign
date_fromdatetimeYESStart timestamp (ISO 8601 UTC)
date_todatetimeYESEnd timestamp (ISO 8601 UTC)date_from..date_to <= 90 days
agent_ids[]array<integer>optionalMulti-select agent filter (?agent_ids[]=25&agent_ids[]=26)
team_idintegeroptionalFilter by team
timezonestringoptionalGroup by day/hour using this timezoneTZDB name (UTC, Asia/Ho_Chi_Minh, ...) - default UTC
formatstringoptionalResponse formatjson (default) / csv / xlsx (see 9.2)

Common 422 error - range exceeds 90 days:

json
{
  "message": "The given data was invalid.",
  "errors": { "date_to": ["The date_from..date_to range may not exceed 90 days."] }
}

Summary of the 9 endpoints

EndpointContent
GET /api/telesales/reports/dashboardOverall KPIs: dials, answer rate, AHT, conversion
GET /api/telesales/reports/disposition-breakdownPercentage breakdown per disposition code
GET /api/telesales/reports/agent-performancePer-agent KPIs: calls, conversions, AHT, talk time
GET /api/telesales/reports/funnelFunnel: Lead -> Dialed -> Connected -> Talked > 30 s -> Converted
GET /api/telesales/reports/caller-id-healthCaller-ID rotation health (ASR, short-call ratio, carrier)
GET /api/telesales/reports/hour-dow-heatmap24-hour x 7-day matrix (best calling windows)
GET /api/telesales/reports/lead-source-attributionConversion by lead source
GET /api/telesales/reports/time-seriesTime series by ?group_by=day|week|hour
GET /api/telesales/reports/abandon-rateRealtime abandon rate (PDPL/TCPA gate)

9.1 Sample responses

GET /api/telesales/reports/dashboard?campaign_id=43&date_from=2026-06-01&date_to=2026-06-10

Response fields

FieldTypeDescriptionValid values
total_dialsintegerTotal dial attempts in the time window>= 0
total_answersintegerTotal calls answered by the customer>= 0
answer_rate_pctstringDecimal string (2 digits) - total_answers / total_dials x 100"0.00"-"100.00"
avg_talk_time_secstringAverage talk time (seconds)
total_talk_time_secstringTotal talk time (seconds)
unique_agentsintegerDistinct agents that dialed in the window>= 0
pdpl_blocked_countintegerCalls blocked by PDPL (outside allowed window)>= 0
json
{
  "data": {
    "total_dials":        487,
    "total_answers":      312,
    "answer_rate_pct":   "64.07",
    "avg_talk_time_sec": "184.5",
    "total_talk_time_sec": "57563",
    "unique_agents":       12,
    "pdpl_blocked_count":   5
  }
}

GET /api/telesales/reports/agent-performance?campaign_id=43&date_from=...&date_to=...

Response fields (each item in data[])

FieldTypeDescriptionValid values
agent_idintegerAgent user ID
namestringAgent full name
usernamestringLogin name
dialsintegerDial attempts in the window>= 0
answersintegerAnswered calls>= 0
avg_aht_secintegerAHT (Average Handle Time) - average per-call handling time (seconds)>= 0
conversionsintegerSuccessful conversions>= 0
conv_ratenumberconversions / answers x 100 (float, NOT string)0-100
json
{
  "data": [
    {
      "agent_id":     3,
      "name":         "Nguyen Van A",
      "username":     "agent01",
      "dials":        152,
      "answers":       98,
      "avg_aht_sec":  191,
      "conversions":   28,
      "conv_rate":   18.42
    },
    {
      "agent_id":     4,
      "name":         "Tran Thi B",
      "username":     "agent02",
      "dials":        140,
      "answers":       86,
      "avg_aht_sec":  176,
      "conversions":   22,
      "conv_rate":   15.71
    }
  ]
}

GET /api/telesales/reports/funnel?campaign_id=43&date_from=...&date_to=...

Returns a flat array (not a nested object {stages, conversion_overall_pct}).

Response fields (each item in data[])

FieldTypeDescriptionValid values
stagestringFunnel stage nameTotal leads / Dialed / Connected / Talked > 30s / Disposed / Converted
countintegerNumber of leads/calls at this stage>= 0
json
{
  "data": [
    { "stage": "Total leads",  "count":  487 },
    { "stage": "Dialed",       "count":  312 },
    { "stage": "Connected",    "count":  248 },
    { "stage": "Talked > 30s", "count":  198 },
    { "stage": "Disposed",     "count":  290 },
    { "stage": "Converted",    "count":   78 }
  ]
}

There are 6 stages - including Disposed. The client computes conversion_overall_pct = Converted / Total_leads x 100 from the first and last stages.

GET /api/telesales/reports/disposition-breakdown?campaign_id=43&date_from=...&date_to=...

Response fields (each item in data[])

FieldTypeDescriptionValid values
codestringDisposition code
labelstringDisplay label
categorystringOutcome groupcontact / no_contact / callback / remove
colorstringUI badge color#RRGGBB
countintegerCalls with this disposition>= 0
pctstringDecimal string (2 digits) - percentage of total calls"0.00"-"100.00"
json
{
  "data": [
    {
      "code":     "no_answer",
      "label":    "No Answer",
      "category": "no_contact",
      "color":    "#6B7280",
      "count":     95,
      "pct":      "30.45"
    },
    {
      "code":     "sale",
      "label":    "Sale Success",
      "category": "contact",
      "color":    "#10B981",
      "count":     78,
      "pct":      "25.00"
    }
  ]
}

Each item includes category + color (the UI uses these to render badges). pct is a decimal string. There is no meta: {total_calls} wrapper - the client computes total = sum(count).

GET /api/telesales/reports/caller-id-health?date_from=...&date_to=...

Response fields (each item in data[])

FieldTypeDescriptionValid values
cidstringCaller-ID number
display_namestring|nullInternal name
carrierstringTelcoviettel / mobifone / vinaphone / vietnamobile / landline / other
cli_typestringNumber typemobile / fixed_02x / hotline_1900 / hotline_1800 / brandname
pool_statusstringStatus inside the rotation poolactive / cooldown / paused / disabled
cooldown_untildatetime|nullWhen cooldown endsISO 8601 UTC
daily_capintegerDaily call cap>= 1
total_callsintegerTotal calls dialed in the window>= 0
answered_callsintegerAnswered calls>= 0
asr_pctnumberAnswer-Seizure Ratio (%) - answered/total x 1000-100
acd_secintegerAverage Call Duration - average talk time (seconds)>= 0
short_callsintegerCalls shorter than the "meaningful conversation" threshold>= 0
short_call_ratio_pctnumberRatio of short calls / total0-100
rejected_callsintegerRejected calls>= 0
health_statusstringHealth classificationhealthy / warning / suspected_block / no_data
health_calls_todayintegerCalls today (used for cooldown evaluation)>= 0
json
{
  "data": [
    {
      "cid":                  "0900000012",
      "display_name":         "Sales Line 1",
      "carrier":              "viettel",
      "cli_type":             "fixed_02x",
      "pool_status":          "active",
      "cooldown_until":        null,
      "daily_cap":             200,
      "total_calls":           152,
      "answered_calls":         98,
      "asr_pct":                64.5,
      "acd_sec":                121,
      "short_calls":             28,
      "short_call_ratio_pct":   18.4,
      "rejected_calls":           2,
      "health_status":         "healthy",
      "health_calls_today":      87
    }
  ]
}

GET /api/telesales/reports/hour-dow-heatmap?campaign_id=43&date_from=...&date_to=...

Returns a flat array of 168 items (7 days x 24 hours), not nested by day-of-week.

Response fields (each item in data[])

FieldTypeDescriptionValid values
day_of_weekintegerDay of the week0 = Sun, 1 = Mon, ..., 6 = Sat
hourintegerHour of the day (per the timezone query param)0-23
dialsintegerDial attempts in the slot>= 0
answeredintegerAnswered calls>= 0
answer_rate_pctnumberAnswer rate answered/dials x 1000-100
json
{
  "data": [
    { "day_of_week": 1, "hour": 0, "dials": 0,  "answered": 0,  "answer_rate_pct":  0 },
    { "day_of_week": 1, "hour": 1, "dials": 0,  "answered": 0,  "answer_rate_pct":  0 },
    { "day_of_week": 1, "hour": 9, "dials": 87, "answered": 56, "answer_rate_pct": 64.4 }
  ]
}

The client pivots into a 7 x 24 matrix to render the heatmap.

GET /api/telesales/reports/lead-source-attribution?date_from=...&date_to=...

Response fields (each item in data[])

FieldTypeDescriptionValid values
sourcestringLead source - leads without a source are grouped under "(unknown)"
total_leadsintegerTotal leads from this source in the window>= 0
dialed_countintegerLeads that have been dialed>= 0
contacted_countintegerLeads that picked up>= 0
conv_countintegerSuccessful conversions>= 0
conv_rate_pctstringConversion rate conv_count/total_leads x 100 (decimal, 2 digits)"0.00"-"100.00"
json
{
  "data": [
    {
      "source":           "facebook_lead_ads",
      "total_leads":       240,
      "dialed_count":      230,
      "contacted_count":   152,
      "conv_count":         62,
      "conv_rate_pct":    "25.83"
    },
    {
      "source":           "(unknown)",
      "total_leads":         5,
      "dialed_count":        3,
      "contacted_count":     2,
      "conv_count":          0,
      "conv_rate_pct":     "0.00"
    }
  ]
}

GET /api/telesales/reports/time-series?campaign_id=43&group_by=day&date_from=...&date_to=...

Returns a flat array - no {group_by, metrics, series} wrapper.

Additional query parameter

FieldTypeRequiredDescriptionValid values
group_bystringoptionalTime grouping unitday (default) / week / hour

Response fields (each item in data[])

FieldTypeDescriptionValid values
bucketstringTime bucket - format depends on group_byday -> YYYY-MM-DD; week -> YYYY-Www (ISO 8601); hour -> YYYY-MM-DD HH:00:00
dialsintegerDial attempts in the bucket>= 0
answersintegerAnswered calls>= 0
talk_secintegerTotal talk time (seconds)>= 0
conversionsintegerSuccessful conversions>= 0
json
{
  "data": [
    { "bucket": "2026-06-01", "dials": 110, "answers": 68, "talk_sec":  9180, "conversions": 18 },
    { "bucket": "2026-06-02", "dials": 105, "answers": 71, "talk_sec": 10260, "conversions": 16 },
    { "bucket": "2026-06-05", "dials":  82, "answers": 55, "talk_sec":  6720, "conversions": 15 }
  ]
}

GET /api/telesales/reports/abandon-rate?campaign_id=43 (realtime)

This endpoint's response is richer than other reports - it contains current (live snapshot) + breaches (history of recent threshold breaches).

Response fields

FieldTypeDescriptionValid values
current.campaign_idintegerCampaign ID
current.attempts_totalintegerTotal attempts in the measurement window>= 0
current.attempts_abandonedintegerAttempts abandoned before the agent could speak>= 0
current.abandon_rate_pctnumberAbandon rate (%)0-100
current.window_minutesintegerMeasurement window length (minutes, default 30 days = 43,200)>= 1
current.window_fromdatetimeWindow startISO 8601
current.window_todatetimeWindow endISO 8601
current.campaign_namestringCampaign name
current.campaign_statusstringCampaign statusdraft / active / paused / completed
current.limit_pctnumberAllowed PDPL/TCPA thresholddefault 3
current.breachedboolWhether the threshold is being breached now
breaches[]arrayHistory of snapshots that breached the threshold (empty if never)
json
{
  "data": {
    "current": {
      "campaign_id":           43,
      "attempts_total":         1,
      "attempts_abandoned":     0,
      "abandon_rate_pct":       0,
      "window_minutes":     43200,
      "window_from":      "2026-05-10 02:31:14",
      "window_to":        "2026-06-09 02:31:14",
      "tenant_id":              1,
      "campaign_name":     "Outbound Sales Q3-2026 03",
      "campaign_status":   "active",
      "limit_pct":              3,
      "breached":           false
    },
    "breaches": []
  }
}

When breached=true, the breaches array contains the history of breaching snapshots (time windows that exceeded the limit). The client uses this to render a warning banner and trend chart.

9.2 Export CSV / Excel

http
GET /api/telesales/reports/{report}/export?format=csv|xlsx

{report} in {dashboard, disposition-breakdown, agent-performance, funnel, caller-id-health, hour-dow-heatmap, lead-source-attribution, time-series}.

Response: a file stream (Content-Type: text/csv or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet).

The default export layout is the generic one. Per-customer Excel templates (column order, header labels, conditional formatting) are configured inside the separate per-customer configuration package.

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