English
English
Appearance
English
English
Appearance
Manage leads (bulk import, list, blacklist/DNC) and customer-defined TTS variables used in the template_text.
POST /api/autocall/campaigns/{id}/leads/importPath params:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
id | integer | Yes | Target campaign ID | Positive integer existing in the account |
Body:
{
"leads": [
{
"phone": "0912345678",
"payload": {
"name": "Nguyễn Văn An",
"amount": "500000",
"callback_date": "2026-06-30",
"order_id": "ORD-12345"
}
},
{
"phone": "0987654321",
"payload": {
"name": "Trần Thị Bích",
"amount": "1200000",
"callback_date": "2026-07-05"
}
}
]
}Body fields:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
leads | array | Yes | Array of leads to push into the campaign | Up to 5000 items per request |
leads[].phone | string | Yes | Lead phone number, Vietnamese format | 0xxx / 84xxx / +84xxx (auto-normalized). Legacy alias phone_number is also accepted |
leads[].payload | object | Yes (if the template uses variables) | JSON object containing TTS variables keyed by variable_code. Legacy alias variables is also accepted | snake_case keys matching defined variable_code values |
Field phone: required, Vietnamese format. Internally stored in E.164 84xxx (for dedup + DNC matching), but GET endpoints return the original 0xxx format to match the client input. The legacy alias phone_number is still accepted in request bodies for backward compatibility — RECOMMENDED to use phone for new integrations.
Example
The client sends "phone": "0987654321" → the GET response returns "phone_number": "0987654321". The system stores E.164 84987654321 for dedup with DNC. Search ?phone=0987 is normalized automatically.
Field payload: a JSON object containing TTS variables keyed by the variable_code previously defined in the pool (see Variables). Keys must match variable_code (lowercase snake_case). The legacy alias variables is accepted.
Behaviour:
skipped_duplicateskipped_dncskipped_invalid, reason invalid_phone_formatscript.template_text placeholders → skipped → counted in skipped_invalid, reason missing_variablesstatus=pending; the audio render job dispatches asynchronouslyactive → the engine picks the lead as soon as the audio is readyValidate variables vs template_text
The system extracts placeholders from the campaign's script.template_text → every such key MUST be present in the lead's payload. Any missing key → the lead is rejected and logged into errors[] (max 20 samples). This ensures the audio render succeeds for every lead.
Response 200 (with some leads missing variables):
{
"data": {
"inserted": 1,
"skipped_invalid": 2,
"skipped_duplicate": 0,
"skipped_dnc": 0,
"errors": [
{
"index": 0,
"phone": "0912345678",
"reason": "missing_variables",
"missing": ["total_amount", "appointment_date"],
"hint": "Script template_text requires variables: {{total_amount}}, {{appointment_date}} — the payload is missing them."
},
{ "index": 487, "phone": "0912abc", "reason": "invalid_phone_format" }
]
}
}Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data.inserted | integer | Number of leads inserted successfully | >= 0 |
data.skipped_invalid | integer | Leads skipped because invalid (bad phone format / missing variable) | >= 0 |
data.skipped_duplicate | integer | Leads skipped because of a duplicate phone in the same campaign | >= 0 |
data.skipped_dnc | integer | Leads skipped because they are on the DNC list | >= 0 |
data.errors[] | array | Sample errors (up to 20 items) | — |
data.errors[].index | integer | Position of the lead in the request leads array | 0-indexed |
data.errors[].phone | string | Phone number of the failed lead | — |
data.errors[].reason | string | Skip reason | missing_variables / invalid_phone_format / duplicate / dnc_blocked |
data.errors[].missing | string[] | Missing variables (only with reason=missing_variables) | — |
data.errors[].hint | string | Hint for fixing the error | — |
Bulk limit
Bulk lead push is capped at 5000 leads per request (chunk client-side if you have more). Lead import rate limit: 5 batches/min/token.
GET /api/autocall/campaigns/{id}/leads?status=pending&page=1 Path params:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
id | integer | Yes | Campaign ID | Positive integer |
Query params:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
status | string | string[] | No | Filter by lead status (single or status[]=a&status[]=b) | pending / dialing / connected / completed / failed / abandoned / blacklisted |
phone | string | No | Fuzzy search by phone number (auto-normalized to 84xxx) | Digits |
page | integer | No | Page number | >= 1, default 1 |
per_page | integer | No | Leads per page | 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
}Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data[].id | integer | Lead ID | Positive integer |
data[].campaign_id | integer | Campaign ID | — |
data[].phone_number | string | Phone number in E.164 format (84xxx) | — |
data[].payload | object | TTS variables received at import time | snake_case keys |
data[].status | string | Lead lifecycle status | pending / dialing / connected / completed / failed / abandoned / blacklisted |
data[].attempt_count | integer | Number of times dialed | >= 0 |
data[].last_attempt_at | datetime ISO 8601 | null | Most recent attempt time (UTC) | — |
data[].last_hangup_cause | string | null | Hangup cause of the last attempt | NORMAL_CLEARING / NO_ANSWER / ... |
data[].next_retry_at | datetime ISO 8601 | null | Next scheduled retry time (UTC) | null if no further retry |
data[].final_audio_path | string | null | Path to the rendered WAV (server-local) | — |
data[].created_at | datetime ISO 8601 | Lead import time (UTC) | — |
current_page | integer | Current page | >= 1 |
last_page | integer | Last page | >= 1 |
per_page | integer | Items per page | 1-200 |
total | integer | Total leads matching the filter | >= 0 |
GET /api/autocall/leads?campaign_id=36&phone=0912&status[]=completed&status[]=failed Cross-campaign lead listing. The response shape is the same as the endpoint above (data[] + pagination).
Query params:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
campaign_id | integer | No | Filter by campaign | Existing campaign ID |
phone | string | No | Fuzzy search by phone number | — |
status[] | string[] | No | Multi-status filter | pending / dialing / connected / completed / failed / abandoned / blacklisted |
from | datetime ISO 8601 | No | Filter by created_at >= from | YYYY-MM-DD or ISO 8601 |
to | datetime ISO 8601 | No | Filter by created_at <= to | YYYY-MM-DD or ISO 8601 |
per_page | integer | No | Leads per page | 1-200, default 50 |
POST /api/autocall/leads/blacklistBody:
{
"phones": ["0912345678", "0987654321"],
"lead_ids": [78912, 78913],
"reason": "Customer requested no further calls",
"source": "customer_request"
}Body fields:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
phones | string[] | No (one of two) | Phone numbers to blacklist | Vietnamese format (0xxx / 84xxx) |
lead_ids | integer[] | No (one of two) | Lead IDs — corresponding phones are auto-resolved and merged into phones | Existing lead IDs |
reason | string | No | Reason for blacklisting (stored in the DNC log) | Up to 500 characters |
source | string | No | Blacklist source | manual / customer_request / duplicate / complaint / other (default manual) |
Response 200:
{ "data": { "blacklisted": 2 } }Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data.blacklisted | integer | Number of phones inserted into the DNC (after dedup) | >= 0 |
POST /api/autocall/leads/unblacklist
{ "phones": ["0912345678"] }Body fields:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
phones | string[] | Yes | Phones to remove from the DNC | Vietnamese format |
Response 200: {"data": {"unblacklisted": 1}}
Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data.unblacklisted | integer | Number of phones removed from the DNC | >= 0 |
POST /api/autocall/leads/bulk-rerender
{ "lead_ids": [78912, 78913] }Re-dispatch the audio render job for the given leads (e.g. when the script has changed and needs to be re-rendered).
Body fields:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
lead_ids | integer[] | Yes | Lead IDs whose audio should be re-rendered | Existing lead IDs in the account |
Response 200: {"data": {"dispatched": 2}}
Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data.dispatched | integer | Number of render jobs queued | >= 0 |
GET /api/autocall/dnc?phone=0912&source=manual Query params:
| Field | Type | Required | Description | Valid values |
|---|---|---|---|---|
phone | string | No | Fuzzy search by phone number | — |
source | string | No | Filter by blacklist source | manual / customer_request / duplicate / complaint / other |
page | integer | No | Page number | >= 1, default 1 |
per_page | integer | No | Items per page | 1-200, default 50 |
Response 200:
{
"data": [
{
"id": 543,
"phone_number": "84912345678",
"source": "customer_request",
"reason": "Customer request",
"blocked_by": 7,
"created_at": "2026-06-24T07:40:00Z"
}
],
"current_page": 1,
"last_page": 12,
"per_page": 50,
"total": 567
}Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data[].id | integer | DNC record ID | Positive integer |
data[].phone_number | string | Phone number in E.164 (84xxx) | — |
data[].source | string | Blacklist source | manual / customer_request / duplicate / complaint / other |
data[].reason | string | null | Blacklist reason | — |
data[].blocked_by | integer | null | User ID who blocked the number | — |
data[].created_at | datetime ISO 8601 | Blacklist time (UTC) | — |
current_page | integer | Current page | >= 1 |
last_page | integer | Last page | >= 1 |
per_page | integer | Items per page | 1-200 |
total | integer | Total records matching the filter | >= 0 |
DNC is global per account
A number on the DNC list is skipped from EVERY campaign belonging to that account.
Customer-defined variables used inside template_text. At render time the system resolves according to its data_type → applying the right formatting (money → spelled-out amount, date → "the fifteenth of July"...).
GET /api/autocall/variablesResponse 200:
{
"data": [
{
"id": 1,
"code": "name",
"label": "Customer name",
"data_type": "name",
"description": "Given name of the customer"
},
{
"id": 2,
"code": "salutation_name",
"label": "Salutation + name",
"data_type": "salutation_name",
"description": "e.g. Mr. An, Ms. Bich"
},
{
"id": 3,
"code": "amount",
"label": "Amount",
"data_type": "money",
"description": "Amount in VND, read out in words"
}
]
}Response fields:
| Field | Type | Description | Valid values |
|---|---|---|---|
data[].id | integer | Variable ID | Positive integer |
data[].code | string | Variable key (used as a payload key when importing leads, and as in template_text) | snake_case, max 64 chars, unique per account, immutable |
data[].label | string | Localized display name | Up to 255 characters |
data[].data_type | string | Data type so TTS reads the value correctly | name / salutation_name / salutation_fullname / fullname / date / time / number / money |
data[].description | string | null | User-facing description | Up to 2000 characters |
Supported data types:
data_type | Description | Input example | Spoken output |
|---|---|---|---|
name | Short name | "Nam" | "Nam" |
salutation_name | Salutation + name | "anh Nam" | "anh Nam" |
salutation_fullname | Salutation + full name | "anh Nguyễn Văn Nam" | full |
fullname | Full name | "Nguyễn Văn Nam" | full |
date | Date Y-m-d | "2026-06-30" | "the thirtieth of June, two thousand twenty-six" |
time | Time H:i | "14:30" | "fourteen thirty" |
number | Integer | "1234" | "one thousand two hundred thirty-four" |
money | VND amount | "500000" | "five hundred thousand dong" |
POST /api/autocall/variables (admin tier)
PUT /api/autocall/variables/{id}
DELETE /api/autocall/variables/{id}Create/update body:
{
"code": "order_id",
"label": "Order code",
"data_type": "name",
"description": "Format ORD-xxxxx",
"example_value": "ORD-12345",
"sort_order": 100,
"is_active": true
}| Field | Required | Type | Default | Description |
|---|---|---|---|---|
code | Yes (create) | string, max 64, snake_case | — | Variable key, unique per account, immutable once created |
label | Yes | string, max 255 | — | Display name |
data_type | Yes | enum (8 types, see table above) | — | Data type so TTS reads the value correctly |
description | No | string, max 2000 | null | Description |
example_value | No | string | null | Sample value used for TTS preview |
sort_order | No | integer | 999 | Display order in the UI picker |
is_active | No | boolean | true | Disable the variable (does NOT delete) — scripts still render but the UI hides it |
is_builtin: true (read-only) marks built-in system variables — they cannot be deleted nor have their code changed.
POST /api/autocall/variables response 201:
{
"data": {
"id": 15,
"code": "order_id",
"label": "Order code",
"data_type": "name",
"description": "Format ORD-xxxxx",
"example_value": "ORD-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: same shape as the POST response (updated object). Send only fields you want to change (partial update). The code field is immutable — the system ignores it if sent.
DELETE /api/autocall/variables/{id} response 200:
{ "data": { "deleted": true } }Response 422 (built-in variable):
{ "message": "Cannot delete a built-in variable (is_builtin=true)" }Response 422 (create validation — e.g. duplicate code):
{
"message": "The given data was invalid.",
"errors": {
"code": ["The code has already been taken."]
}
}