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 cho từng 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ệ
filefile✅File 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": [
    ["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) ​

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ệ
filefile✅File 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": "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) ​

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_tokenstring✅Token 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": "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 ​

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

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

FieldTypeMô tả
full_namestringHọ tên (max 255 ký tự)
emailstringEmail hợp lệ (max 255)
phonestringSố điện thoại (được chuẩn hoá lại về 84xxx nếu sửa)
priority_scorenumberMức ưu tiên (0–100)
next_attempt_atdatetimeThời điểm dự kiến gọi lại (ISO 8601 UTC)
consent_statusstringgranted / withdrawn / unknown
consent_sourcestringNguồn ghi nhận đồng ý
assigned_agent_idinteger|nullAgent phụ trách
custom_fieldsobjectObject {code: value}. Merge vào giá trị cũ — trường không truyền được giữ nguyên; gửi {code: null} để xoá.
commit_modestring"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ĩaHTTPHà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+ invalidChỉ ghi khi 100% custom_fields hợp lệ. Nếu có invalid → từ chối cả trường cố định.
partialChấp nhận phần valid200 nếu tất cả valid, 207 Multi-Status nếu có invalidGhi trường cố định + phần custom_fields valid, bỏ qua invalid.
validate_onlyChỉ kiểm tra, không ghi200 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ụ ​

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

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

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

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

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

ruleTriggerField bổ sung
requiredField bắt buộc null/rỗng—
unknown_fieldCode không có định nghĩa—
type_mismatchKhông đúng typeexpected_type
max_lengthChuỗi quá dàimax
min_value / max_valueSố ngoài khoảngmin / max
regex_mismatchKhông khớp regexexpected_pattern
email_invalidEmail sai syntax—
not_in_optionsSelect/multiselect không thuộc optionsallowed_options
duplicateVi phạm is_uniqueconflicting_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):

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

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 (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 qua POST /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 ​

FieldTypeRequiredMô tảGiá trị hợp lệ
lead_idsinteger[]✅Danh sách lead ID cần update1-1000 phần tử
patchobject✅Field 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ẽ 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:

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ử
reasonstring✅Lý 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'). 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:

json
{
  "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ảnXử lý
Gửi campaign_code="X" và tenant đã có campaign name=XPush 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ạiAuto-tạo campaign mới name=X, status=draft, dialer_mode=manual, gắn dataset từ URL
Bỏ trốngAuto-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

  1. Xem/tạo campaign đích trên UI /telesales/campaigns (hoặc gọi GET /api/telesales/campaigns để list).
  2. Lấy name của campaign đích → dùng làm campaign_code cố định trong mọi request push của integration đó.
  3. 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_code khá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.

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

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

json
{ "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 normalize 0xxx → 84xxx.
  • lead_data (nếu có) phải khớp schema field phase=lead củ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"
  • 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-After header

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.

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

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

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

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

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

json
{
  "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_sec trong response body

Khi nào dùng /leads/single vs /leads bulk? ​

Use caseEndpoint đề 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.

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