Skip to content

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)

http
POST /api/telesales/campaigns/{id}/leads/preview
Content-Type: multipart/form-data

file=@call_list.csv

Request form fields (multipart)

FieldTypeRequiredMô tảGiá trị hợp lệ
filefileFile CSV (UTF-8) hoặc XLSX cần previewmax 10MB

Response body fields

FieldTypeMô tảGiá trị hợp lệ
headersstring[]Danh sách header phát hiện ở hàng đầu
samplearray[]Tối đa 5 hàng dữ liệu mẫu
mapping_suggestionsobject[]Gợi ý mapping cột → field hệ thống
mapping_suggestions[].indexintegerVị trí cột (0-based)
mapping_suggestions[].headerstringTên cột
mapping_suggestions[].samplestringMẫu dữ liệu
mapping_suggestions[].suggestedstring|nullField hệ thống được gợi ý mappingfull_name / phone / email / null
row_countintegerTổng số dòng (không kể header)

Response 200:

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

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

FieldTypeRequiredMô tảGiá trị hợp lệ
filefileFile CSV/XLSX cần dry-runmax 10MB
mappingstring (JSON)Mapping cột → field hệ thốngObject JSON {phone, full_name?, email?, custom_fields?}

Response body fields

FieldTypeMô tảGiá trị hợp lệ
file_tokenstringToken tạm thời định danh file đã upload — truyền lại ở bước commitDạng telesales/imports/{campaign_id}/{uuid}.csv
summaryobjectTổng kết kết quả validate
summary.totalintegerTổng dòng
summary.validintegerSố dòng hợp lệ
summary.duplicateintegerSố dòng trùng (phone đã có trong chiến dịch)
summary.dncintegerSố dòng phone đang ở DNC
summary.errorsarrayDanh sách lỗi format
sample_rowsarrayTối đa 50 dòng mẫu kèm trạng thái phân loại
sample_rows[].rowintegerSố dòng trong file (1-based)
sample_rows[].phonestringSố điện thoại
sample_rows[].full_namestringHọ tên (nếu có map)
sample_rows[].statusstringPhân loại dòngvalid / duplicate / dnc / error
sample_rows[].reasonstringLý do (chỉ có khi status không phải valid)

Response 200:

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

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

FieldTypeRequiredMô tảGiá trị hợp lệ
file_tokenstringToken trả về từ bước dry-runToken còn hiệu lực (TTL 24h)
mappingstring (JSON)Mapping cột → field — phải khớp dry-runCùng định dạng bước 2

Response body fields

FieldTypeMô tảGiá trị hợp lệ
campaign_idintegerID chiến dịch
insertedintegerSố lead vừa insert
skipped_duplicateintegerBỏ qua do trùng
skipped_dncintegerBỏ qua do thuộc DNC
skipped_invalidintegerBỏ qua do format sai
took_msintegerThời gian xử lý (ms)

Response 201:

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

http
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

EndpointMục đích
GET /api/telesales/campaigns/{id}/leadsDanh 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/bulkCập nhật hàng loạt
DELETE /api/telesales/leads/bulkXoá 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

http
GET /api/telesales/campaigns/36/leads?page=1&per_page=50&status=pending&sort=priority_score&dir=desc

Query parameters

FieldTypeRequiredMô tảGiá trị hợp lệ
statusstringoptionalLọc theo trạng thái leadpending / queued / contacted / converted / dnc / exhausted
assigned_agent_idintegeroptionalLọc theo agent đang được giaoUser ID
searchstringoptionalTìm theo phone / phone_normalized / full_name / email (LIKE %keyword%)
sortstringoptionalCột sortpriority_score (default) / imported_at / last_attempt_at
dirstringoptionalHướng sortasc / desc (default desc)
pageintegeroptionalSố trang≥ 1 (default 1)
per_pageintegeroptionalBản ghi mỗi trang1-200 (default 50)

Response fields (mỗi item trong data[])

FieldTypeMô tảGiá trị hợp lệ
idintegerID lead
campaign_idintegerID chiến dịch
full_namestring|nullHọ tên lead
phonestringSố điện thoại (gốc — định dạng nhập vào)
statusstringTrạng thái leadpending / queued / contacted / converted / dnc / exhausted
priority_scoreintegerMức ưu tiên gọi — càng cao càng được xếp trước0-100
assigned_agent_idinteger|nullID agent đang được giao (sticky)
attempts_countintegerSố lần đã thử quay số
next_attempt_atdatetime|nullThời điểm dự kiến gọi lại (ISO 8601 UTC)

Response 200:

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

FieldTypeMô tảGiá trị hợp lệ
idintegerID lead
campaign_idintegerID chiến dịch
full_namestring|nullHọ tên lead
phonestringSố điện thoại gốc
statusstringTrạng thái leadpending / queued / contacted / converted / dnc / exhausted
priority_scoreintegerMức ưu tiên0-100
next_attempt_atdatetime|nullThời điểm dự kiến gọi lại
assigned_agent_idinteger|nullAgent sticky
attempts_countintegerSố lần đã thử
custom_fieldsobjectCác field mở rộng — key khớp định nghĩa trường tuỳ chỉnh (entity Lead)
created_atdatetimeThời điểm tạo (ISO 8601 UTC)

Response 200:

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

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

FieldTypeMô tảGiá trị hợp lệ
full_namestringHọ tênmax 150 ký tự
emailstringEmailPhải đúng định dạng email
custom_fieldsobjectField mở rộng — key khớp định nghĩa trường tuỳ chỉnhObject JSON
priority_scoreintegerMức ưu tiên0-100
next_attempt_atdatetimeThời điểm dự kiến gọi lại (ISO 8601 UTC)
consent_statusstringTrạng thái đồng ýgranted / withdrawn / unknown
consent_sourcestringNguồ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:

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

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

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

FieldTypeRequiredMô tảGiá trị hợp lệ
lead_idsinteger[]Danh sách lead ID cần update1-1000 phần tử
patchobjectField cần cập nhật cho toàn bộ lead trong danh sáchXem bảng patch.*
patch.statusstringoptionalTrạng thái mớipending / queued / contacted / converted / dnc / exhausted
patch.assigned_agent_idinteger|nulloptionalSticky agentUser ID hoặc null để gỡ
patch.priority_scoreintegeroptionalMức ưu tiên0-100
patch.next_attempt_atdatetimeoptionalThời điểm gọi lạiISO 8601 UTC

Body:

json
{
  "lead_ids": [78912, 78913, 78920, 78921],
  "patch": {
    "status": "queued",
    "assigned_agent_id": 25,
    "priority_score": 100
  }
}

Response body fields

FieldTypeMô tảGiá trị hợp lệ
successboolTổng quan thành côngtrue / false
updatedintegerSố lead update thực tế
skippedintegerSố lead bị bỏ qua (không thuộc tài khoản hoặc validate fail)

Response 200:

json
{
  "success": true,
  "updated": 4,
  "skipped": 0
}

Response 422 — lead_ids rỗng hoặc quá lớn:

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

json
{ "lead_ids": [78912, 78913, 78920] }

Response 200:

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

FieldTypeRequiredMô tảGiá trị hợp lệ
lead_idsinteger[]Danh sách lead ID cần đẩy vào DNC1-1000 phần tử
reasonstringLý do đưa vào DNC — bắt buộc cho audit trailmax 500 ký tự

Body:

json
{
  "lead_ids": [78912, 78913],
  "reason": "Customer requested opt-out"
}

Response body fields

FieldTypeMô tảGiá trị hợp lệ
successboolTổng quan thành côngtrue / false
added_to_dncintegerSố phone vừa thêm vào DNC
skipped_already_dncintegerSố phone đã ở DNC từ trước
audit_trail_idstringID bản ghi audit (nhật ký audit DNC)Dạng audit_dnc_*

Response 200:

json
{
  "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 status chuyển sang 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:

json
{
  "message": "The given data was invalid.",
  "errors": { "reason": ["The reason field is required."] }
}

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