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 cho từng 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": [
["Alice Sample","0900000002","0900000003","REF-001","High-value prospect"],
["Tran Hoa","0987654321","","REF-002",""]
],
"mapping_suggestions": [
{ "index": 0, "header": "Customer Name", "sample": "Alice Sample", "suggested": "full_name" },
{ "index": 1, "header": "Primary Phone", "sample": "0900000002", "suggested": "phone" },
{ "index": 2, "header": "Alt. Phone", "sample": "0900000003", "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": "0900000002", "full_name": "Alice Sample", "status": "valid" },
{ "row": 5, "phone": "0900000000", "full_name": "Le Linh", "status": "dnc",
"reason": "Phone on DNC since 2026-04-01" }
]
}Bước 3 — Commit (lưu vào cơ sở dữ liệu)
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": "0900000002",
"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": "0900000002",
"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": "0900000003",
"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
Chấp nhận cả trường cố định lẫn custom_fields. Khi request có custom_fields, server sẽ chạy qua pipeline kiểm tra theo định nghĩa (xem Trường tuỳ chỉnh) và trả về custom_fields_report chi tiết.
Request body
| Field | Type | Mô tả |
|---|---|---|
full_name | string | Họ tên (max 255 ký tự) |
email | string | Email hợp lệ (max 255) |
phone | string | Số điện thoại (được chuẩn hoá lại về 84xxx nếu sửa) |
priority_score | number | 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 | granted / withdrawn / unknown |
consent_source | string | Nguồn ghi nhận đồng ý |
assigned_agent_id | integer|null | Agent phụ trách |
custom_fields | object | Object {code: value}. Merge vào giá trị cũ — trường không truyền được giữ nguyên; gửi {code: null} để xoá. |
commit_mode | string | "strict" (mặc định) | "partial" | "validate_only" — xem bảng dưới. |
Các trường không thể cập nhật qua endpoint này.
campaign_id, status — dùng API chuyển campaign / cập nhật trạng thái riêng.
3 chế độ commit
commit_mode | Ý nghĩa | HTTP | Hành vi |
|---|---|---|---|
strict (mặc định) | Toàn bộ hoặc không gì | 200 nếu tất cả valid, 422 nếu có 1+ invalid | Chỉ ghi khi 100% custom_fields hợp lệ. Nếu có invalid → từ chối cả trường cố định. |
partial | Chấp nhận phần valid | 200 nếu tất cả valid, 207 Multi-Status nếu có invalid | Ghi trường cố định + phần custom_fields valid, bỏ qua invalid. |
validate_only | Chỉ kiểm tra, không ghi | 200 luôn (nếu request đúng format) | Không ghi dữ liệu xuống cơ sở dữ liệu. — trả breakdown để client hiển thị lỗi trước khi commit thật. |
Body ví dụ
{
"full_name": "Nguyen Thi Mai Anh",
"email": "mai.anh@example.com",
"commit_mode": "strict",
"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 — success (strict, tất cả valid) — 200
{
"data": { "...": "full Lead — xem GET /api/telesales/leads/{id}" },
"commit_mode": "strict",
"custom_fields_report": {
"summary": {
"total_fields_submitted": 2,
"valid_fields": 2,
"invalid_fields": 0,
"valid_field_codes": ["reference_code", "note"],
"invalid_field_codes": []
},
"errors": []
}
}Response — strict, có invalid — 422
{
"message": "One or more custom fields are invalid. Fix them and resend, or use commit_mode=partial to persist only the valid subset.",
"commit_mode": "strict",
"custom_fields_report": {
"summary": { "total_fields_submitted": 3, "valid_fields": 2, "invalid_fields": 1,
"valid_field_codes": ["reference_code","note"], "invalid_field_codes": ["policy_status"] },
"errors": {
"policy_status": {
"rule": "not_in_options",
"value": "zzz",
"allowed_options": ["active","grace_period","lapsed","reinstated","surrendered"],
"message": "Policy status is not in the allowed list."
}
}
}
}Response — partial — 207
{
"data": { "...": "Lead với phần custom_fields valid đã được ghi" },
"commit_mode": "partial",
"custom_fields_report": {
"summary": { "total_fields_submitted": 3, "valid_fields": 2, "invalid_fields": 1 },
"errors": { "policy_status": { "...": "..." } }
}
}Response — validate_only — 200 (không ghi DB)
{
"data": { "...": "Lead hiện tại, chưa đổi" },
"custom_fields_report": { "summary": { "...": "..." }, "errors": { "...": "..." } }
}Bảng rule types (giá trị của errors[code].rule)
rule | Trigger | Field bổ sung |
|---|---|---|
required | Field bắt buộc null/rỗng | — |
unknown_field | Code không có định nghĩa | — |
type_mismatch | Không đúng type | expected_type |
max_length | Chuỗi quá dài | max |
min_value / max_value | Số ngoài khoảng | min / max |
regex_mismatch | Không khớp regex | expected_pattern |
email_invalid | Email sai syntax | — |
not_in_options | Select/multiselect không thuộc options | allowed_options |
duplicate | Vi phạm is_unique | conflicting_record_id |
Response — không có custom_fields
Nếu request body không chứa custom_fields, response trả 200 với object Lead thuần (không có custom_fields_report, commit_mode) — giữ nguyên hành vi cho integration không dùng trường tuỳ chỉnh.
Response 422 (trường cố định):
{ "message": "The given data was invalid.", "errors": { "priority_score": ["The priority score must be between 0 and 100."] } }
{ "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 (chỉ trường cố định)
Áp dụng cùng một giá trị cho nhiều lead — chỉ hỗ trợ trường cố định (status, assigned_agent_id, priority_score, next_attempt_at). Endpoint này không chấp nhận custom_fields — nếu gửi trong body sẽ bị bỏ qua.
Với nhu cầu chỉnh custom_fields cho nhiều lead:
- Nhỏ (< 20 lead): gọi
PATCH /api/telesales/leads/{id}từng lead — mỗi call có báo cáo chi tiết theo từng trường. - Lớn: xuất qua
GET /api/telesales/campaigns/{id}/leads/export, chỉnh CSV, import lại quaPOST /api/telesales/campaigns/{id}/leads/import(có sẵn 3 bước preview → dry-run → commit với pipeline kiểm tra đầy đủ).
Request body
| 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ẽ tự động bị bỏ qua và được thống kê trong trường \skipped. (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'). Trạng thái Lead được cập nhật thành \dnc. - 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."] }
}4.3 Push lead từ hệ thống ngoài (Ingest API)
Nhóm 2 endpoint public để hệ thống ngoài (CRM, landing page, marketing platform...) gửi lead vào chiến dịch Zorio Telesales.
Auth: Sanctum Personal Access Token (Bearer) với ability telesales:push.
Trước khi push lead, cần biết dataset_code — schema field áp dụng cho campaign đích. Xem trang Bộ dữ liệu (Datasets) để lấy dataset_code + schema.
Về field campaign_code trong request
campaign_code là định danh tự khai của client để chọn/tạo chiến dịch nhận lead — không phải mã do hệ thống Zorio sinh sẵn. Cả 2 endpoint push (bulk + single) đều nhận field này (nullable, max 64 ký tự).
| Kịch bản | Xử lý |
|---|---|
Gửi campaign_code="X" và tenant đã có campaign name=X | Push vào campaign đó, đồng thời tự gắn dataset_id nếu campaign chưa có |
Gửi campaign_code="X" chưa tồn tại | Auto-tạo campaign mới name=X, status=draft, dialer_mode=manual, gắn dataset từ URL |
| Bỏ trống | Auto-tạo campaign name="API-{YYYYMMDD-HHMMSS}" — không khuyến khích cho production vì mỗi request sinh 1 campaign mới, khó báo cáo |
Best practice
- Xem/tạo campaign đích trên UI
/telesales/campaigns(hoặc gọiGET /api/telesales/campaignsđể list). - Lấy
namecủa campaign đích → dùng làmcampaign_codecố định trong mọi request push của integration đó. - Nếu có nhiều nguồn dữ liệu tách biệt (VD landing page A vs webhook CRM B), dùng
campaign_codekhác nhau để dễ track hiệu suất từng nguồn.
Response 2 endpoint trả block campaign chứa id, code, name (code và name giống nhau — do bảng telesales_campaigns không có column code riêng, giá trị = tên campaign).
POST /api/v1/telesales/datasets/{dataset_code}/leads — Push lead từ hệ thống ngoài
Endpoint dùng cho đối tác đẩy lead từ CRM/hệ thống nội bộ vào Zorio. Auth qua Sanctum PAT với ability telesales:push.
POST /api/v1/telesales/datasets/sample_life_renewal/leads
Authorization: Bearer <api_token>
Content-Type: application/json
{
"leads": [
{
"full_name": "Alice Sample",
"phone": "0900000001",
"lead_data": {
"policy_number": "POL-2024-0001",
"annual_premium": 12000000,
"policy_status": "lapsed"
}
},
{
"full_name": "Bob Sample",
"phone": "0900000002",
"lead_data": {
"policy_number": "POL-2024-0002",
"annual_premium": 15000000
}
}
],
"campaign_code": "SAMPLE_RENEWAL_202608"
}Response 200:
{
"ok": true,
"inserted": 2,
"skipped_duplicate": 0,
"skipped_dnc": 0,
"errors": [],
"dataset": {
"id": 5,
"code": "sample_life_renewal",
"name": "FinanceCorp Life Insurance — Renewal"
},
"campaign": {
"id": 123,
"code": "SAMPLE_RENEWAL_202608",
"created": false
}
}Block dataset trong response cho phép client tracking bộ dữ liệu nào đã được ánh xạ.
Error 404 · dataset_not_found
Nếu {dataset_code} trong URL không khớp bộ dữ liệu nào của tenant, endpoint trả HTTP 404:
{ "error": "dataset_not_found" }Kiểm tra code qua GET /api/telesales/datasets trước. Code mẫu sample_life_renewal chỉ minh hoạ — cần thay bằng dataset code có thực trong tenant của bạn.
Validation
- Mỗi lead phải có
phone(bắt buộc). Auto normalize0xxx→84xxx. lead_data(nếu có) phải khớp schema fieldphase=leadcủa bộ dữ liệu:- Field không tồn tại → row bị bỏ qua với
errors[i].error = "unknown_field" - Field required thiếu → row bị bỏ qua với
errors[i].error = "missing_required" - Data type sai → row bị bỏ qua với
errors[i].error = "invalid_type"
- Field không tồn tại → row bị bỏ qua với
- Phone trùng trong campaign hiện tại →
skipped_duplicate++ - Phone nằm trong DNC list →
skipped_dnc++ - Batch limit: 500 lead/request. Batch lớn hơn cần chia nhiều request.
Rate limit
- 60 request/phút/token
- Tenant vượt quota → HTTP 429 với
Retry-Afterheader
POST /api/v1/telesales/datasets/{dataset_code}/leads/single — Push 1 lead (dedicated single)
Endpoint chuyên dụng gửi 1 lead mỗi request — phù hợp use case realtime (webhook từ landing page, API tích hợp CRM 1-by-1). Payload phẳng, response REST-ful với HTTP status chuẩn.
POST /api/v1/telesales/datasets/maplife_bhnt/leads/single
Authorization: Bearer <api_token>
Content-Type: application/json
{
"full_name": "Alice Sample",
"phone": "0900000001",
"email": "alice@example.com",
"lead_data": {
"policy_number": "POL-2024-0001",
"annual_premium": 12000000
},
"campaign_code": "SAMPLE_RENEWAL_202608"
}Response 201 · Created
{
"data": {
"id": 15401,
"campaign_id": 123,
"full_name": "Alice Sample",
"phone": "0900000001",
"phone_normalized": "84900000001",
"email": "alice@example.com",
"status": "pending",
"lead_data": {
"full_name": "Alice Sample",
"phone": "0900000001",
"policy_number": "POL-2024-0001",
"annual_premium": 12000000
},
"created_at": "2026-08-16T12:45:00+00:00"
},
"dataset": {
"id": 5,
"code": "maplife_bhnt",
"name": "FinanceCorp Life Insurance — Renewal"
},
"campaign": {
"id": 123,
"code": "SAMPLE_RENEWAL_202608",
"name": "SAMPLE_RENEWAL_202608",
"created": false
}
}Response 409 · duplicate_phone
Số điện thoại đã tồn tại trong chiến dịch (dedup trong campaign, không cross-campaign):
{
"error": "duplicate_phone",
"message": "Số điện thoại 0900000001 đã tồn tại trong chiến dịch",
"existing_lead_id": 15380
}Response 403 · phone_in_dnc
Số điện thoại nằm trong danh sách Do-Not-Call của tổ chức:
{
"error": "phone_in_dnc",
"message": "Số điện thoại nằm trong danh sách không gọi (DNC) của tổ chức"
}Response 422 · Validation
Định dạng phone sai:
{
"error": "phone_format_invalid",
"message": "Số điện thoại không đúng định dạng — cần dạng 0xxx hoặc 84xxx"
}lead_data không khớp schema dataset:
{
"error": "invalid_lead_data",
"message": "Trường 'policy_number' không hợp lệ: field_type_mismatch",
"errors": [
{ "row": 0, "field": "policy_number", "error": "field_type_mismatch", "expected": "text" }
]
}Response 404 · dataset_not_found
{dataset_code} không tồn tại — xem GET /api/telesales/datasets để lấy code hợp lệ.
Rate limit riêng
- 300 request/phút/token — cao hơn endpoint bulk (60/min) vì use case single realtime cần throughput cao
- Bucket độc lập với bulk endpoint — không xung đột quota
- Vượt quota → HTTP 429 với
retry_after_sectrong response body
Khi nào dùng /leads/single vs /leads bulk?
| Use case | Endpoint đề xuất |
|---|---|
| Webhook realtime từ landing page → 1 lead ngay | /leads/single |
| Import file CSV/Excel → N lead cùng lúc | /leads (bulk) |
| Sync từ CRM ngoài — mỗi lead mới tạo → 1 API call | /leads/single |
| Job scheduled đêm hôm — batch 500 lead/lần | /leads (bulk) |
Nguyên tắc: nếu client có N > 10 lead cùng lúc và có thể chờ vài giây → dùng bulk. Nếu N=1 event-driven → dùng single.
