Leads + Variables
Quản lý lead (import bulk, list, blacklist/DNC) và biến TTS do khách hàng tự định nghĩa dùng trong template template_text.
Push lead vào chiến dịch (bulk)
POST /api/autocall/campaigns/{id}/leads/importPath params:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
id | integer | Có | ID chiến dịch nhận lead | Số nguyên dương tồn tại trong tài khoản |
Body:
{
"leads": [
{
"phone": "0912345678",
"payload": {
"name": "Nguyễn Văn An",
"amount": "500000",
"callback_date": "2026-06-30",
"loan_id": "LN-12345"
}
},
{
"phone": "0987654321",
"payload": {
"name": "Trần Thị Bích",
"amount": "1200000",
"callback_date": "2026-07-05"
}
}
]
}Body fields:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
leads | array | Có | Mảng lead cần push vào chiến dịch | Tối đa 5000 item/request |
leads[].phone | string | Có | Số điện thoại lead, format Việt Nam | 0xxx / 84xxx / +84xxx (auto-normalize). Legacy alias phone_number được chấp nhận |
leads[].payload | object | Có (nếu template có biến) | JSON object chứa biến TTS theo variable_code. Legacy alias variables được chấp nhận | Key snake_case khớp variable_code đã định nghĩa |
Field phone: bắt buộc, format Việt Nam. DB nội bộ lưu format E.164 84xxx (cho dedup + DNC matching) nhưng API GET trả về format gốc 0xxx để khớp với input client. Legacy alias phone_number vẫn được chấp nhận trong body request cho backward compat — KHUYẾN NGHỊ dùng phone cho integration mới.
Ví dụ
Client gửi "phone": "0987654321" -> response GET trả "phone_number": "0987654321". Hệ thống lưu format E.164 84987654321 để dedup với DNC. Search ?phone=0987 được hệ thống tự normalize.
Field payload: JSON object chứa biến TTS theo variable_code đã định nghĩa trong pool (xem Variables). Key phải match variable_code (lowercase, snake_case). Legacy alias variables được chấp nhận.
Behaviour:
- Lead trùng phone trong cùng campaign -> skip -> đếm vào
skipped_duplicate - Lead nằm trong DNC -> skip -> đếm vào
skipped_dnc - Phone format không hợp lệ -> skip -> đếm vào
skipped_invalid, lý doinvalid_phone_format - Payload thiếu biến required trong
script.template_textplaceholder-> skip -> đếm vàoskipped_invalid, lý domissing_variables - Lead inserted với
status=pending, audio render job dispatch async - Nếu campaign đang
active-> engine pick lead ngay khi audio sẵn sàng
Validate variables vs template_text
Hệ thống extract placeholder từ script.template_text của campaign -> mọi key này PHẢI có trong payload của lead. Thiếu key nào -> reject lead, log vào errors[] (max 20 sample). Đảm bảo audio render thành công cho mọi lead.
Response 200 (case có lead thiếu biến):
{
"data": {
"inserted": 1,
"skipped_invalid": 2,
"skipped_duplicate": 0,
"skipped_dnc": 0,
"errors": [
{
"index": 0,
"phone": "0912345678",
"reason": "missing_variables",
"missing": ["tong_can_thanh_toan", "ngay_den_han"],
"hint": "Script template_text yêu cầu các biến: {{tong_can_thanh_toan}}, {{ngay_den_han}} — payload đang thiếu các biến này."
},
{ "index": 487, "phone": "0912abc", "reason": "invalid_phone_format" }
]
}
}Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data.inserted | integer | Số lead insert thành công | >= 0 |
data.skipped_invalid | integer | Số lead bị skip do invalid (sai phone format / thiếu biến) | >= 0 |
data.skipped_duplicate | integer | Số lead bị skip do trùng phone trong cùng chiến dịch | >= 0 |
data.skipped_dnc | integer | Số lead bị skip do nằm trong DNC | >= 0 |
data.errors[] | array | Danh sách lỗi mẫu (tối đa 20 item) | — |
data.errors[].index | integer | Vị trí lead trong array leads của request | 0-indexed |
data.errors[].phone | string | Số điện thoại lead lỗi | — |
data.errors[].reason | string | Lý do skip | missing_variables / invalid_phone_format / duplicate / dnc_blocked |
data.errors[].missing | string[] | Danh sách biến thiếu (chỉ với reason=missing_variables) | — |
data.errors[].hint | string | Gợi ý fix lỗi | — |
Bulk limit
Lead push bulk tối đa 5000 lead/request (chunk client-side nếu nhiều hơn). Rate limit lead import: 5 batch/phút/token.
List leads
GET /api/autocall/campaigns/{id}/leads?status=pending&page=1
Path params:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
id | integer | Có | ID chiến dịch | Số nguyên dương |
Query params:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
status | string | string[] | Không | Lọc theo trạng thái lead (single hoặc dùng status[]=a&status[]=b) | pending / dialing / connected / completed / failed / abandoned / blacklisted |
phone | string | Không | Tìm gần đúng theo số điện thoại (auto-normalize 84xxx) | Chuỗi số |
page | integer | Không | Trang phân trang | >= 1, default 1 |
per_page | integer | Không | Số lead mỗi trang | 1-200, default 50 |
Response 200:
{
"data": [
{
"id": 78912,
"campaign_id": 36,
"phone_number": "84912345678",
"payload": {"name": "Nguyễn Văn An", "amount": "500000"},
"status": "completed",
"attempt_count": 1,
"last_attempt_at": "2026-06-24T08:32:00Z",
"last_hangup_cause": "NORMAL_CLEARING",
"next_retry_at": null,
"final_audio_path": "/audio/render/lead_78912.wav",
"created_at": "2026-06-24T07:35:00Z"
}
],
"current_page": 1,
"last_page": 10,
"per_page": 50,
"total": 487
}Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data[].id | integer | ID lead | Số nguyên dương |
data[].campaign_id | integer | ID chiến dịch | — |
data[].phone_number | string | Số điện thoại format E.164 (84xxx) | — |
data[].payload | object | Biến TTS đã nhận khi import | Key snake_case |
data[].status | string | Trạng thái lifecycle của lead | pending / dialing / connected / completed / failed / abandoned / blacklisted |
data[].attempt_count | integer | Số lần đã gọi | >= 0 |
data[].last_attempt_at | datetime ISO 8601 | null | Thời điểm attempt gần nhất (UTC) | — |
data[].last_hangup_cause | string | null | Hangup cause của attempt cuối | NORMAL_CLEARING / NO_ANSWER / ... |
data[].next_retry_at | datetime ISO 8601 | null | Thời điểm lên lịch retry tiếp theo (UTC) | null nếu không retry tiếp |
data[].final_audio_path | string | null | Đường dẫn file wav render xong (server local) | — |
data[].created_at | datetime ISO 8601 | Thời điểm import lead (UTC) | — |
current_page | integer | Trang hiện tại | >= 1 |
last_page | integer | Số trang cuối | >= 1 |
per_page | integer | Số item mỗi trang | 1-200 |
total | integer | Tổng số lead khớp filter | >= 0 |
GET /api/autocall/leads?campaign_id=36&phone=0912&status[]=completed&status[]=failed
Liệt kê lead cross-campaign. Response shape giống endpoint trên (data[] + pagination).
Query params:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
campaign_id | integer | Không | Lọc theo chiến dịch | ID chiến dịch tồn tại |
phone | string | Không | Tìm gần đúng theo số điện thoại | — |
status[] | string[] | Không | Lọc nhiều trạng thái | pending / dialing / connected / completed / failed / abandoned / blacklisted |
from | datetime ISO 8601 | Không | Lọc theo created_at >= from | YYYY-MM-DD hoặc ISO 8601 |
to | datetime ISO 8601 | Không | Lọc theo created_at <= to | YYYY-MM-DD hoặc ISO 8601 |
per_page | integer | Không | Số lead mỗi trang | 1-200, default 50 |
Bulk action
Blacklist
POST /api/autocall/leads/blacklistBody:
{
"phones": ["0912345678", "0987654321"],
"lead_ids": [78912, 78913],
"reason": "Khách hàng yêu cầu không gọi",
"source": "customer_request"
}Body fields:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
phones | string[] | Không (1 trong 2) | Danh sách số điện thoại cần blacklist | Format Việt Nam (0xxx / 84xxx) |
lead_ids | integer[] | Không (1 trong 2) | Danh sách lead ID — phone tương ứng được auto-resolve và merge vào phones | ID lead tồn tại |
reason | string | Không | Lý do blacklist (lưu vào DNC log) | Tối đa 500 ký tự |
source | string | Không | Nguồn gốc blacklist | manual / customer_request / duplicate / complaint / other (default manual) |
Response 200:
{ "data": { "blacklisted": 2 } }Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data.blacklisted | integer | Số phone đã insert vào DNC (đã dedup) | >= 0 |
Unblacklist
POST /api/autocall/leads/unblacklist
{ "phones": ["0912345678"] }Body fields:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
phones | string[] | Có | Danh sách phone cần xoá khỏi DNC | Format Việt Nam |
Response 200: {"data": {"unblacklisted": 1}}
Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data.unblacklisted | integer | Số phone đã xoá khỏi DNC | >= 0 |
Bulk re-render audio
POST /api/autocall/leads/bulk-rerender
{ "lead_ids": [78912, 78913] }Dispatch lại job render audio cho các lead (vd khi script đổi -> cần render lại).
Body fields:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
lead_ids | integer[] | Có | Danh sách lead ID cần render lại audio | ID lead tồn tại trong tài khoản |
Response 200: {"data": {"dispatched": 2}}
Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data.dispatched | integer | Số job render đã dispatch vào queue | >= 0 |
Do-Not-Call list
GET /api/autocall/dnc?phone=0912&source=manual
Query params:
| Field | Type | Required | Mô tả | Giá trị hợp lệ |
|---|---|---|---|---|
phone | string | Không | Tìm gần đúng theo số điện thoại | — |
source | string | Không | Lọc theo nguồn blacklist | manual / customer_request / duplicate / complaint / other |
page | integer | Không | Trang phân trang | >= 1, default 1 |
per_page | integer | Không | Số item mỗi trang | 1-200, default 50 |
Response 200:
{
"data": [
{
"id": 543,
"phone_number": "84912345678",
"source": "customer_request",
"reason": "Khách yêu cầu",
"blocked_by": 7,
"created_at": "2026-06-24T07:40:00Z"
}
],
"current_page": 1,
"last_page": 12,
"per_page": 50,
"total": 567
}Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data[].id | integer | ID record DNC | Số nguyên dương |
data[].phone_number | string | Số điện thoại format E.164 (84xxx) | — |
data[].source | string | Nguồn blacklist | manual / customer_request / duplicate / complaint / other |
data[].reason | string | null | Lý do blacklist | — |
data[].blocked_by | integer | null | User ID người thực hiện chặn | — |
data[].created_at | datetime ISO 8601 | Thời điểm blacklist (UTC) | — |
current_page | integer | Trang hiện tại | >= 1 |
last_page | integer | Số trang cuối | >= 1 |
per_page | integer | Số item mỗi trang | 1-200 |
total | integer | Tổng số record khớp filter | >= 0 |
DNC global per tài khoản
Số trong DNC sẽ bị skip khỏi MỌI campaign của khách hàng đó.
Variables (biến TTS do khách hàng tự định nghĩa)
Biến do khách hàng định nghĩa dùng trong template_text. Khi render, hệ thống resolve theo data_type -> format đúng (số tiền -> đọc thành chữ, ngày -> "ngày 15 tháng 7"...).
List variables
GET /api/autocall/variablesResponse 200:
{
"data": [
{
"id": 1,
"code": "name",
"label": "Tên khách hàng",
"data_type": "name",
"description": "Tên gọi của khách hàng"
},
{
"id": 2,
"code": "salutation_name",
"label": "Xưng hô + Tên",
"data_type": "salutation_name",
"description": "VD: anh An, chị Bích"
},
{
"id": 3,
"code": "amount",
"label": "Số tiền",
"data_type": "money",
"description": "Số tiền VND, sẽ đọc thành chữ"
}
]
}Field response:
| Field | Type | Mô tả | Giá trị hợp lệ |
|---|---|---|---|
data[].id | integer | ID variable | Số nguyên dương |
data[].code | string | Mã biến (dùng làm key trong payload khi import lead, dùng làm trong template_text) | snake_case, max 64 ký tự, unique per tài khoản, immutable |
data[].label | string | Tên hiển thị tiếng Việt | Tối đa 255 ký tự |
data[].data_type | string | Loại dữ liệu để TTS đọc đúng format | name / salutation_name / salutation_fullname / fullname / date / time / number / money |
data[].description | string | null | Mô tả cho người dùng | Tối đa 2000 ký tự |
Data types hỗ trợ:
data_type | Mô tả | Ví dụ input | Output đọc |
|---|---|---|---|
name | Tên rút gọn | "Nam" | "Nam" |
salutation_name | Xưng hô + Tên | "anh Nam" | "anh Nam" |
salutation_fullname | Xưng hô + Họ tên | "anh Nguyễn Văn Nam" | đầy đủ |
fullname | Họ tên | "Nguyễn Văn Nam" | đầy đủ |
date | Ngày Y-m-d | "2026-06-30" | "ngày ba mươi tháng sáu năm hai nghìn không trăm hai mươi sáu" |
time | Giờ H:i | "14:30" | "mười bốn giờ ba mươi phút" |
number | Số nguyên | "1234" | "một nghìn hai trăm ba mươi tư" |
money | Số tiền VND | "500000" | "năm trăm nghìn đồng" |
CRUD variables (admin only)
POST /api/autocall/variables (admin tier)
PUT /api/autocall/variables/{id}
DELETE /api/autocall/variables/{id}Body create/update:
{
"code": "loan_id",
"label": "Mã hợp đồng",
"data_type": "name",
"description": "Format LN-xxxxx",
"example_value": "LN-12345",
"sort_order": 100,
"is_active": true
}| Field | Required | Type | Default | Mô tả |
|---|---|---|---|---|
code | Có (create) | string, max 64, snake_case | — | Variable key, unique per tài khoản, immutable sau khi tạo |
label | Có | string, max 255 | — | Tên hiển thị |
data_type | Có | enum (8 type, xem bảng phía trên) | — | Loại dữ liệu để TTS đọc đúng format |
description | Không | string, max 2000 | null | Mô tả |
example_value | Không | string | null | Giá trị mẫu dùng cho TTS preview |
sort_order | Không | integer | 999 | Thứ tự hiển thị trong UI picker |
is_active | Không | boolean | true | Tắt variable (KHÔNG xoá) — script vẫn render nhưng UI ẩn |
is_builtin: true (read-only) đánh dấu các variable hệ thống cung cấp sẵn — KHÔNG cho phép xoá hoặc đổi code.
POST /api/autocall/variables response 201:
{
"data": {
"id": 15,
"code": "loan_id",
"label": "Mã hợp đồng",
"data_type": "name",
"description": "Format LN-xxxxx",
"example_value": "LN-12345",
"sort_order": 100,
"is_active": true,
"is_builtin": false,
"tenant_id": 1,
"created_at": "2026-06-25T03:00:00Z",
"updated_at": "2026-06-25T03:00:00Z"
}
}PUT /api/autocall/variables/{id} response 200: shape giống POST response (object đã update). Chỉ truyền field cần đổi (partial update). Field code immutable — hệ thống bỏ qua nếu gửi.
DELETE /api/autocall/variables/{id} response 200:
{ "data": { "deleted": true } }Response 422 (variable hệ thống):
{ "message": "Không thể xoá biến hệ thống (is_builtin=true)" }Response 422 (validation create — vd code trùng):
{
"message": "The given data was invalid.",
"errors": {
"code": ["The code has already been taken."]
}
}