Khách hàng tiềm năng (Leads)
Nhóm endpoint quản lý lead trong từng chiến dịch: import 3 bước (preview → dry-run → commit), CRUD đơn lẻ, bulk update / delete / mark DNC, cùng cơ chế custom field linh hoạt qua JSON.
4.1 Import lead
Định dạng file
CSV (UTF-8) hoặc XLSX. Có template tổng quát CallList_Template.xlsx; template chuyên biệt theo từng ngành nghề được cấp trong gói cấu hình riêng riêng cho khách hàng.
Quy trình 3 bước:
Bước 1 — Preview (xem cấu trúc cột)
POST /api/telesales/campaigns/{id}/leads/preview
Content-Type: multipart/form-data
file=@call_list.csvRequest form fields (multipart)
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
file | file | ✅ | File CSV (UTF-8) hoặc XLSX cần preview | max 10MB |
Response body fields
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
headers | string[] | Danh sách header phát hiện ở hàng đầu | |
sample | array[] | Tối đa 5 hàng dữ liệu mẫu | |
mapping_suggestions | object[] | Gợi ý mapping cột → field hệ thống | |
mapping_suggestions[].index | integer | Vị trí cột (0-based) | |
mapping_suggestions[].header | string | Tên cột | |
mapping_suggestions[].sample | string | Mẫu dữ liệu | |
mapping_suggestions[].suggested | string|null | Field hệ thống được gợi ý mapping | full_name / phone / email / null |
row_count | integer | Tổng số dòng (không kể 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
}Bước 2 — Dry-run (phát hiện trùng / DNC / lỗi)
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}}Định dạng mapping: giá trị là index cột 0-based trong hàng header của CSV. phone là bắt buộc (số nguyên). Có thể map bất kỳ số cột phụ nào vào custom_fields.* — key trong đây PHẢI khớp field code đã đăng ký ở định nghĩa trường tuỳ chỉnh cho entity=Lead.
Request form fields (multipart)
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
file | file | ✅ | File CSV/XLSX cần dry-run | max 10MB |
mapping | string (JSON) | ✅ | Mapping cột → field hệ thống | Object JSON {phone, full_name?, email?, custom_fields?} |
Response body fields
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
file_token | string | Token tạm thời định danh file đã upload — truyền lại ở bước commit | Dạng telesales/imports/{campaign_id}/{uuid}.csv |
summary | object | Tổng kết kết quả validate | |
summary.total | integer | Tổng dòng | |
summary.valid | integer | Số dòng hợp lệ | |
summary.duplicate | integer | Số dòng trùng (phone đã có trong chiến dịch) | |
summary.dnc | integer | Số dòng phone đang ở DNC | |
summary.errors | array | Danh sách lỗi format | |
sample_rows | array | Tối đa 50 dòng mẫu kèm trạng thái phân loại | |
sample_rows[].row | integer | Số dòng trong file (1-based) | |
sample_rows[].phone | string | Số điện thoại | |
sample_rows[].full_name | string | Họ tên (nếu có map) | |
sample_rows[].status | string | Phân loại dòng | valid / duplicate / dnc / error |
sample_rows[].reason | string | Lý do (chỉ có khi status không phải 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" }
]
}Bước 3 — Commit (ghi vào DB)
POST /api/telesales/campaigns/{id}/leads/import
Content-Type: multipart/form-data
file_token=telesales/imports/36/abc123.csv
mapping={...giống bước 2...}Request form fields (multipart)
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
file_token | string | ✅ | Token trả về từ bước dry-run | Token còn hiệu lực (TTL 24h) |
mapping | string (JSON) | ✅ | Mapping cột → field — phải khớp dry-run | Cùng định dạng bước 2 |
Response body fields
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
campaign_id | integer | ID chiến dịch | |
inserted | integer | Số lead vừa insert | |
skipped_duplicate | integer | Bỏ qua do trùng | |
skipped_dnc | integer | Bỏ qua do thuộc DNC | |
skipped_invalid | integer | Bỏ qua do format sai | |
took_ms | integer | Thời gian xử lý (ms) |
Response 201:
{
"data": {
"campaign_id": 36,
"inserted": 462,
"skipped_duplicate": 18,
"skipped_dnc": 7,
"skipped_invalid": 0,
"took_ms": 1840
}
}Khai báo custom field
Hệ thống lưu dữ liệu mở rộng của mỗi lead trong cột JSON trường tuỳ chỉnh của lead. Trước khi import, admin của khách hàng phải khai báo các key hợp lệ qua:
GET /api/custom-fields?entity=Lead
POST /api/custom-fields
PATCH /api/custom-fields/{id}
DELETE /api/custom-fields/{id}Mỗi định nghĩa gồm entity, code, type (string / integer / decimal / date / datetime / boolean / enum), luật validate, thứ tự hiển thị và (tuỳ chọn) is_encrypted=true để bật pipeline mã hoá Layer 2. Thêm field mới không cần migrate schema.
4.2 Lead CRUD
| Endpoint | Mục đích |
|---|---|
GET /api/telesales/campaigns/{id}/leads | Danh sách lead (có phân trang + lọc: ?status=pending&assigned_agent_id=25) |
GET /api/telesales/leads/{id} | Chi tiết (kèm JSON custom_fields) |
PATCH /api/telesales/leads/{id} | Cập nhật |
DELETE /api/telesales/leads/{id} | Xoá |
PATCH /api/telesales/leads/bulk | Cập nhật hàng loạt |
DELETE /api/telesales/leads/bulk | Xoá hàng loạt |
POST /api/telesales/leads/bulk-dnc | Đẩy hàng loạt vào DNC |
GET /api/telesales/campaigns/{id}/leads — danh sách lead
GET /api/telesales/campaigns/36/leads?page=1&per_page=50&status=pending&sort=priority_score&dir=descQuery parameters
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
status | string | optional | Lọc theo trạng thái lead | pending / queued / contacted / converted / dnc / exhausted |
assigned_agent_id | integer | optional | Lọc theo agent đang được giao | User ID |
search | string | optional | Tìm theo phone / phone_normalized / full_name / email (LIKE %keyword%) | |
sort | string | optional | Cột sort | priority_score (default) / imported_at / last_attempt_at |
dir | string | optional | Hướng sort | asc / desc (default desc) |
page | integer | optional | Số trang | ≥ 1 (default 1) |
per_page | integer | optional | Bản ghi mỗi trang | 1-200 (default 50) |
Response fields (mỗi item trong data[])
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
id | integer | ID lead | |
campaign_id | integer | ID chiến dịch | |
full_name | string|null | Họ tên lead | |
phone | string | Số điện thoại (gốc — định dạng nhập vào) | |
status | string | Trạng thái lead | pending / queued / contacted / converted / dnc / exhausted |
priority_score | integer | Mức ưu tiên gọi — càng cao càng được xếp trước | 0-100 |
assigned_agent_id | integer|null | ID agent đang được giao (sticky) | |
attempts_count | integer | Số lần đã thử quay số | |
next_attempt_at | datetime|null | Thời điểm dự kiến gọi lại (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 }
}Tối ưu payload
Response của list không kèm custom_fields để giữ payload gọn (487 lead × 5–10 custom field rất lớn). Gọi GET /api/telesales/leads/{id} khi cần chi tiết đầy đủ.
GET /api/telesales/leads/{id} — chi tiết
Response body fields
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
id | integer | ID lead | |
campaign_id | integer | ID chiến dịch | |
full_name | string|null | Họ tên lead | |
phone | string | Số điện thoại gốc | |
status | string | Trạng thái lead | pending / queued / contacted / converted / dnc / exhausted |
priority_score | integer | Mức ưu tiên | 0-100 |
next_attempt_at | datetime|null | Thời điểm dự kiến gọi lại | |
assigned_agent_id | integer|null | Agent sticky | |
attempts_count | integer | Số lần đã thử | |
custom_fields | object | Các field mở rộng — key khớp định nghĩa trường tuỳ chỉnh (entity Lead) | |
created_at | datetime | Thời điểm tạo (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"
}
}Tập key bên trong custom_fields phụ thuộc vào định nghĩa trường tuỳ chỉnh của khách hàng cho entity Lead.
Response 404:
{ "message": "Lead not found." }PATCH /api/telesales/leads/{id} — cập nhật một lead
Request body fields (mọi field optional — chỉ truyền field cần đổi)
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
full_name | string | Họ tên | max 150 ký tự |
email | string | Phải đúng định dạng email | |
custom_fields | object | Field mở rộng — key khớp định nghĩa trường tuỳ chỉnh | Object JSON |
priority_score | integer | Mức ưu tiên | 0-100 |
next_attempt_at | datetime | Thời điểm dự kiến gọi lại (ISO 8601 UTC) | |
consent_status | string | Trạng thái đồng ý | granted / withdrawn / unknown |
consent_source | string | Nguồn ghi nhận đồng ý | max 100 ký tự |
Field bị cấm
Các field sau không cập nhật qua endpoint này: phone, campaign_id, status, assigned_agent_id (dùng API re-assignment riêng).
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: trả về object lead đầy đủ (cùng shape với GET /api/telesales/leads/{id}).
Response 422 — các trường hợp validate sai:
// priority_score nằm ngoài khoảng cho phép
{
"message": "The given data was invalid.",
"errors": { "priority_score": ["The priority score must be between 0 and 100."] }
}
// next_attempt_at sai định dạng
{
"message": "The given data was invalid.",
"errors": { "next_attempt_at": ["The next attempt at is not a valid date."] }
}
// custom_fields không phải array/object
{
"message": "The given data was invalid.",
"errors": { "custom_fields": ["The custom fields must be an array."] }
}
// consent_status không nằm trong enum cho phép
{
"message": "The given data was invalid.",
"errors": { "consent_status": ["The selected consent status is invalid."] }
}
// email sai định dạng
{
"message": "The given data was invalid.",
"errors": { "email": ["The email must be a valid email address."] }
}DELETE /api/telesales/leads/{id} — xoá một lead
Response 204 No Content (thành công).
Response 404: lead không tồn tại hoặc không thuộc tài khoản.
Response 422 — lead đang có cuộc gọi active:
{
"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
Đây là soft delete — lead được set deleted_at nhưng lịch sử quay số vẫn giữ. Bulk delete cũng hoạt động cùng cách.
PATCH /api/telesales/leads/bulk — cập nhật hàng loạt
Request body fields
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
lead_ids | integer[] | ✅ | Danh sách lead ID cần update | 1-1000 phần tử |
patch | object | ✅ | Field cần cập nhật cho toàn bộ lead trong danh sách | Xem bảng patch.* |
patch.status | string | optional | Trạng thái mới | pending / queued / contacted / converted / dnc / exhausted |
patch.assigned_agent_id | integer|null | optional | Sticky agent | User ID hoặc null để gỡ |
patch.priority_score | integer | optional | Mức ưu tiên | 0-100 |
patch.next_attempt_at | datetime | optional | Thời điểm gọi lại | ISO 8601 UTC |
Body:
{
"lead_ids": [78912, 78913, 78920, 78921],
"patch": {
"status": "queued",
"assigned_agent_id": 25,
"priority_score": 100
}
}Response body fields
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
success | bool | Tổng quan thành công | true / false |
updated | integer | Số lead update thực tế | |
skipped | integer | Số lead bị bỏ qua (không thuộc tài khoản hoặc validate fail) |
Response 200:
{
"success": true,
"updated": 4,
"skipped": 0
}Response 422 — lead_ids rỗng hoặc quá lớn:
{
"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."] }
}Giới hạn bulk
Tối đa 1000 lead_ids mỗi request. Lead không thuộc tài khoản sẽ bị bỏ qua âm thầm (cộng vào skipped).
DELETE /api/telesales/leads/bulk — xoá hàng loạt
Body:
{ "lead_ids": [78912, 78913, 78920] }Response 200:
{ "success": true, "deleted": 3 }Response 422: validate giống PATCH /api/telesales/leads/bulk.
POST /api/telesales/leads/bulk-dnc — đánh dấu DNC hàng loạt
Đưa nhiều lead vào DNC list trong một request.
Request body fields
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
lead_ids | integer[] | ✅ | Danh sách lead ID cần đẩy vào DNC | 1-1000 phần tử |
reason | string | ✅ | Lý do đưa vào DNC — bắt buộc cho audit trail | max 500 ký tự |
Body:
{
"lead_ids": [78912, 78913],
"reason": "Customer requested opt-out"
}Response body fields
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
success | bool | Tổng quan thành công | true / false |
added_to_dnc | integer | Số phone vừa thêm vào DNC | |
skipped_already_dnc | integer | Số phone đã ở DNC từ trước | |
audit_trail_id | string | ID bản ghi audit (nhật ký audit DNC) | Dạng audit_dnc_* |
Response 200:
{
"success": true,
"added_to_dnc": 2,
"skipped_already_dnc": 0,
"audit_trail_id": "audit_dnc_abc123"
}Sau khi mark DNC:
- Số điện thoại của lead được insert vào danh sách DNC (
source: 'customer_request'). - Lead
statuschuyển sangdnc. - Mọi callback đang chờ của lead bị huỷ.
- Một bản ghi audit được tạo trong nhật ký audit DNC với
actor_id,lead_ids,reason,audit_trail_id.
Response 422 — thiếu reason:
{
"message": "The given data was invalid.",
"errors": { "reason": ["The reason field is required."] }
}