English
English
Appearance
English
English
Appearance
Endpoints to manage leads within each campaign: 3-step import (preview → dry-run → commit), single CRUD, bulk update / delete / mark DNC, plus a flexible JSON-based custom-field mechanism.
File format
CSV (UTF-8) or XLSX. There is a generic template CallList_Template.xlsx; industry-specific templates ship in the separate per-customer configuration package.
The flow has three steps:
POST /api/telesales/campaigns/{id}/leads/preview
Content-Type: multipart/form-data
file=@call_list.csv| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
file | file | ✅ | CSV (UTF-8) or XLSX file to preview | max 10MB |
| Field | Type | Description | Valid values |
|---|---|---|---|
headers | string[] | Detected header row | |
sample | array[] | Up to 5 sample data rows | |
mapping_suggestions | object[] | Suggested column → system-field mapping | |
mapping_suggestions[].index | integer | Column position (0-based) | |
mapping_suggestions[].header | string | Column name | |
mapping_suggestions[].sample | string | Sample data | |
mapping_suggestions[].suggested | string|null | Suggested system field | full_name / phone / email / null |
row_count | integer | Total rows (excluding header) |
Response 200:
{
"headers": ["Customer Name","Primary Phone","Alt. Phone","Reference Code","Note"],
"sample": [
["Nguyen Mai","0912345678","0988111222","REF-001","High-value prospect"],
["Tran Hoa","0987654321","","REF-002",""]
],
"mapping_suggestions": [
{ "index": 0, "header": "Customer Name", "sample": "Nguyen Mai", "suggested": "full_name" },
{ "index": 1, "header": "Primary Phone", "sample": "0912345678", "suggested": "phone" },
{ "index": 2, "header": "Alt. Phone", "sample": "0988111222", "suggested": null },
{ "index": 3, "header": "Reference Code", "sample": "REF-001", "suggested": null },
{ "index": 4, "header": "Note", "sample": "High-value", "suggested": null }
],
"row_count": 487
}POST /api/telesales/campaigns/{id}/leads/dry-run
Content-Type: multipart/form-data
file=@call_list.csv
mapping={"phone":1,"full_name":0,"custom_fields":{"reference_code":3,"alt_phone":2,"note":4}}mapping format: each value is the 0-based column index in the CSV header row. phone is mandatory (integer). Any additional column can be mapped into custom_fields.* — the key here MUST match a custom-field code registered for entity=Lead.
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
file | file | ✅ | CSV/XLSX file to dry-run | max 10MB |
mapping | string (JSON) | ✅ | Column → system-field mapping | JSON object {phone, full_name?, email?, custom_fields?} |
| Field | Type | Description | Valid values |
|---|---|---|---|
file_token | string | Temporary token identifying the uploaded file — pass back in the commit step | Format telesales/imports/{campaign_id}/{uuid}.csv |
summary | object | Summary of validation results | |
summary.total | integer | Total rows | |
summary.valid | integer | Valid rows | |
summary.duplicate | integer | Duplicate rows (phone already in the campaign) | |
summary.dnc | integer | Rows whose phone is on the DNC list | |
summary.errors | array | List of format errors | |
sample_rows | array | Up to 50 sample rows with classification | |
sample_rows[].row | integer | Row number in the file (1-based) | |
sample_rows[].phone | string | Phone number | |
sample_rows[].full_name | string | Full name (if mapped) | |
sample_rows[].status | string | Row classification | valid / duplicate / dnc / error |
sample_rows[].reason | string | Reason (only when status is not valid) |
Response 200:
{
"file_token": "telesales/imports/36/abc123.csv",
"summary": {
"total": 487,
"valid": 462,
"duplicate": 18,
"dnc": 7,
"errors": []
},
"sample_rows": [
{ "row": 1, "phone": "0912345678", "full_name": "Nguyen Mai", "status": "valid" },
{ "row": 5, "phone": "0900000000", "full_name": "Le Linh", "status": "dnc",
"reason": "Phone on DNC since 2026-04-01" }
]
}POST /api/telesales/campaigns/{id}/leads/import
Content-Type: multipart/form-data
file_token=telesales/imports/36/abc123.csv
mapping={...same as step 2...}| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
file_token | string | ✅ | Token returned by the dry-run step | Still valid (TTL 24h) |
mapping | string (JSON) | ✅ | Column → field mapping — must match the dry-run | Same format as step 2 |
| Field | Type | Description | Valid values |
|---|---|---|---|
campaign_id | integer | Campaign ID | |
inserted | integer | Number of leads inserted | |
skipped_duplicate | integer | Skipped due to duplication | |
skipped_dnc | integer | Skipped due to DNC | |
skipped_invalid | integer | Skipped due to invalid format | |
took_ms | integer | Processing time (ms) |
Response 201:
{
"data": {
"campaign_id": 36,
"inserted": 462,
"skipped_duplicate": 18,
"skipped_dnc": 7,
"skipped_invalid": 0,
"took_ms": 1840
}
}The system stores extended lead data in the lead's custom-field JSON column. Before importing, the customer's admin must declare valid keys via:
GET /api/custom-fields?entity=Lead
POST /api/custom-fields
PATCH /api/custom-fields/{id}
DELETE /api/custom-fields/{id}Each definition includes entity, code, type (string / integer / decimal / date / datetime / boolean / enum), validation rules, display order, and (optionally) is_encrypted=true to enable the Layer-2 encryption pipeline. Adding a new field requires no schema migration.
| Endpoint | Purpose |
|---|---|
GET /api/telesales/campaigns/{id}/leads | List leads (with pagination + filters: ?status=pending&assigned_agent_id=25) |
GET /api/telesales/leads/{id} | Detail (includes the custom_fields JSON) |
PATCH /api/telesales/leads/{id} | Update |
DELETE /api/telesales/leads/{id} | Delete |
PATCH /api/telesales/leads/bulk | Bulk update |
DELETE /api/telesales/leads/bulk | Bulk delete |
POST /api/telesales/leads/bulk-dnc | Bulk-push to DNC |
GET /api/telesales/campaigns/{id}/leads — list leads GET /api/telesales/campaigns/36/leads?page=1&per_page=50&status=pending&sort=priority_score&dir=desc| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
status | string | optional | Filter by lead status | pending / queued / contacted / converted / dnc / exhausted |
assigned_agent_id | integer | optional | Filter by sticky agent | User ID |
search | string | optional | Search by phone / phone_normalized / full_name / email (LIKE %keyword%) | |
sort | string | optional | Sort column | priority_score (default) / imported_at / last_attempt_at |
dir | string | optional | Sort direction | asc / desc (default desc) |
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 | Lead ID | |
campaign_id | integer | Campaign ID | |
full_name | string|null | Lead full name | |
phone | string | Phone number (raw — as entered) | |
status | string | Lead status | pending / queued / contacted / converted / dnc / exhausted |
priority_score | integer | Call priority — higher = called first | 0-100 |
assigned_agent_id | integer|null | Sticky agent ID | |
attempts_count | integer | Number of dial attempts | |
next_attempt_at | datetime|null | Scheduled next attempt (ISO 8601 UTC) |
Response 200:
{
"data": [
{
"id": 78912,
"campaign_id": 36,
"full_name": "Nguyen Thi Mai",
"phone": "0912345678",
"status": "pending",
"priority_score": 85,
"assigned_agent_id": 25,
"attempts_count": 0,
"next_attempt_at": "2026-06-07T08:00:00Z"
}
],
"meta": { "current_page": 1, "last_page": 10, "per_page": 50, "total": 487 }
}Payload optimization
The list response does not include custom_fields to keep the payload compact (487 leads × 5–10 custom fields is huge). Call GET /api/telesales/leads/{id} when you need full details.
GET /api/telesales/leads/{id} — detail | Field | Type | Description | Valid values |
|---|---|---|---|
id | integer | Lead ID | |
campaign_id | integer | Campaign ID | |
full_name | string|null | Lead full name | |
phone | string | Raw phone number | |
status | string | Lead status | pending / queued / contacted / converted / dnc / exhausted |
priority_score | integer | Priority | 0-100 |
next_attempt_at | datetime|null | Scheduled next attempt | |
assigned_agent_id | integer|null | Sticky agent | |
attempts_count | integer | Attempts made | |
custom_fields | object | Extended fields — keys match the custom-field definitions (entity Lead) | |
created_at | datetime | Creation timestamp (ISO 8601 UTC) |
Response 200:
{
"data": {
"id": 78912,
"campaign_id": 36,
"full_name": "Nguyen Thi Mai",
"phone": "0912345678",
"status": "pending",
"priority_score": 85,
"next_attempt_at": "2026-06-07T08:00:00Z",
"assigned_agent_id": 25,
"attempts_count": 0,
"custom_fields": {
"reference_code": "REF-2026-001",
"alt_phone": "0988111222",
"note": "Returning customer, prefers Vietnamese"
},
"created_at": "2026-06-06T06:30:00Z"
}
}The set of keys inside custom_fields depends on the customer's custom-field definitions for the Lead entity.
Response 404:
{ "message": "Lead not found." }PATCH /api/telesales/leads/{id} — update one lead | Field | Type | Description | Valid values |
|---|---|---|---|
full_name | string | Full name | max 150 chars |
email | string | Must be a valid email address | |
custom_fields | object | Extended fields — keys match custom-field definitions | JSON object |
priority_score | integer | Priority | 0-100 |
next_attempt_at | datetime | Scheduled next attempt (ISO 8601 UTC) | |
consent_status | string | Consent status | granted / withdrawn / unknown |
consent_source | string | Consent capture source | max 100 chars |
Forbidden fields
The following fields cannot be updated through this endpoint: phone, campaign_id, status, assigned_agent_id (use the dedicated re-assignment API).
Body:
{
"full_name": "Nguyen Thi Mai Anh",
"email": "mai.anh@example.com",
"custom_fields": {
"reference_code": "REF-2026-001-UPDATED",
"note": "Customer requested callback Monday morning"
},
"priority_score": 92,
"next_attempt_at": "2026-06-09T09:00:00Z",
"consent_status": "granted",
"consent_source": "phone_call_2026-06-08"
}Response 200: returns the full lead object (same shape as GET /api/telesales/leads/{id}).
Response 422 — validation cases:
// priority_score outside the allowed range
{
"message": "The given data was invalid.",
"errors": { "priority_score": ["The priority score must be between 0 and 100."] }
}
// next_attempt_at has invalid format
{
"message": "The given data was invalid.",
"errors": { "next_attempt_at": ["The next attempt at is not a valid date."] }
}
// custom_fields is not an array/object
{
"message": "The given data was invalid.",
"errors": { "custom_fields": ["The custom fields must be an array."] }
}
// consent_status not in the allowed enum
{
"message": "The given data was invalid.",
"errors": { "consent_status": ["The selected consent status is invalid."] }
}
// email is invalid
{
"message": "The given data was invalid.",
"errors": { "email": ["The email must be a valid email address."] }
}DELETE /api/telesales/leads/{id} — delete one lead Response 204 No Content (success).
Response 404: lead does not exist or does not belong to the account.
Response 422 — lead has an active call:
{
"message": "Cannot delete a lead with an active call.",
"errors": { "lead_id": ["Lead 78912 is currently on call UUID xxx — wait for it to end first."] }
}Soft delete
This is a soft delete — the lead's deleted_at is set, but its dial history is preserved. Bulk delete behaves the same way.
PATCH /api/telesales/leads/bulk — bulk update | Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
lead_ids | integer[] | ✅ | Lead IDs to update | 1-1000 items |
patch | object | ✅ | Fields to apply to all leads in the list | See patch.* |
patch.status | string | optional | New status | pending / queued / contacted / converted / dnc / exhausted |
patch.assigned_agent_id | integer|null | optional | Sticky agent | User ID or null to unassign |
patch.priority_score | integer | optional | Priority | 0-100 |
patch.next_attempt_at | datetime | optional | Scheduled callback time | ISO 8601 UTC |
Body:
{
"lead_ids": [78912, 78913, 78920, 78921],
"patch": {
"status": "queued",
"assigned_agent_id": 25,
"priority_score": 100
}
}| Field | Type | Description | Valid values |
|---|---|---|---|
success | bool | Overall success | true / false |
updated | integer | Leads actually updated | |
skipped | integer | Leads skipped (not in account, or validation failed) |
Response 200:
{
"success": true,
"updated": 4,
"skipped": 0
}Response 422 — lead_ids empty or too large:
{
"message": "The given data was invalid.",
"errors": { "lead_ids": ["The lead ids must have at least 1 item.", "The lead ids may not have more than 1000 items."] }
}Bulk limits
Maximum 1000 lead_ids per request. Leads not in the account are silently skipped (counted as skipped).
DELETE /api/telesales/leads/bulk — bulk delete Body:
{ "lead_ids": [78912, 78913, 78920] }Response 200:
{ "success": true, "deleted": 3 }Response 422: validation behaves like PATCH /api/telesales/leads/bulk.
POST /api/telesales/leads/bulk-dnc — bulk-mark DNC Push many leads onto the DNC list in a single request.
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
lead_ids | integer[] | ✅ | Lead IDs to push to DNC | 1-1000 items |
reason | string | ✅ | DNC reason — required for the audit trail | max 500 chars |
Body:
{
"lead_ids": [78912, 78913],
"reason": "Customer requested opt-out"
}| Field | Type | Description | Valid values |
|---|---|---|---|
success | bool | Overall success | true / false |
added_to_dnc | integer | Phones newly added to DNC | |
skipped_already_dnc | integer | Phones already on DNC | |
audit_trail_id | string | Audit record ID (DNC audit log) | Format audit_dnc_* |
Response 200:
{
"success": true,
"added_to_dnc": 2,
"skipped_already_dnc": 0,
"audit_trail_id": "audit_dnc_abc123"
}After marking DNC:
source: 'customer_request').status changes to dnc.actor_id, lead_ids, reason, audit_trail_id.Response 422 — missing reason:
{
"message": "The given data was invalid.",
"errors": { "reason": ["The reason field is required."] }
}