Skip to content

Leads + Variables

Manage leads (bulk import, list, blacklist/DNC) and customer-defined TTS variables used in the template_text.

Push leads into a campaign (bulk)

http
POST /api/autocall/campaigns/{id}/leads/import

Path params:

FieldTypeRequiredDescriptionValid values
idintegerYesTarget campaign IDPositive integer existing in the account

Body:

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

FieldTypeRequiredDescriptionValid values
leadsarrayYesArray of leads to push into the campaignUp to 5000 items per request
leads[].phonestringYesLead phone number, Vietnamese format0xxx / 84xxx / +84xxx (auto-normalized). Legacy alias phone_number is also accepted
leads[].payloadobjectYes (if the template uses variables)JSON object containing TTS variables keyed by variable_code. Legacy alias variables is also acceptedsnake_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:

  • Leads with a duplicate phone within the same campaign → skipped → counted in skipped_duplicate
  • Leads in DNC → skipped → counted in skipped_dnc
  • Invalid phone format → skipped → counted in skipped_invalid, reason invalid_phone_format
  • Payload missing variables required by script.template_text placeholders → skipped → counted in skipped_invalid, reason missing_variables
  • Leads are inserted with status=pending; the audio render job dispatches asynchronously
  • If the campaign is active → the engine picks the lead as soon as the audio is ready

Validate 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):

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

FieldTypeDescriptionValid values
data.insertedintegerNumber of leads inserted successfully>= 0
data.skipped_invalidintegerLeads skipped because invalid (bad phone format / missing variable)>= 0
data.skipped_duplicateintegerLeads skipped because of a duplicate phone in the same campaign>= 0
data.skipped_dncintegerLeads skipped because they are on the DNC list>= 0
data.errors[]arraySample errors (up to 20 items)
data.errors[].indexintegerPosition of the lead in the request leads array0-indexed
data.errors[].phonestringPhone number of the failed lead
data.errors[].reasonstringSkip reasonmissing_variables / invalid_phone_format / duplicate / dnc_blocked
data.errors[].missingstring[]Missing variables (only with reason=missing_variables)
data.errors[].hintstringHint 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.

List leads

GET /api/autocall/campaigns/{id}/leads?status=pending&page=1

Path params:

FieldTypeRequiredDescriptionValid values
idintegerYesCampaign IDPositive integer

Query params:

FieldTypeRequiredDescriptionValid values
statusstring | string[]NoFilter by lead status (single or status[]=a&status[]=b)pending / dialing / connected / completed / failed / abandoned / blacklisted
phonestringNoFuzzy search by phone number (auto-normalized to 84xxx)Digits
pageintegerNoPage number>= 1, default 1
per_pageintegerNoLeads per page1-200, default 50

Response 200:

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

FieldTypeDescriptionValid values
data[].idintegerLead IDPositive integer
data[].campaign_idintegerCampaign ID
data[].phone_numberstringPhone number in E.164 format (84xxx)
data[].payloadobjectTTS variables received at import timesnake_case keys
data[].statusstringLead lifecycle statuspending / dialing / connected / completed / failed / abandoned / blacklisted
data[].attempt_countintegerNumber of times dialed>= 0
data[].last_attempt_atdatetime ISO 8601 | nullMost recent attempt time (UTC)
data[].last_hangup_causestring | nullHangup cause of the last attemptNORMAL_CLEARING / NO_ANSWER / ...
data[].next_retry_atdatetime ISO 8601 | nullNext scheduled retry time (UTC)null if no further retry
data[].final_audio_pathstring | nullPath to the rendered WAV (server-local)
data[].created_atdatetime ISO 8601Lead import time (UTC)
current_pageintegerCurrent page>= 1
last_pageintegerLast page>= 1
per_pageintegerItems per page1-200
totalintegerTotal 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:

FieldTypeRequiredDescriptionValid values
campaign_idintegerNoFilter by campaignExisting campaign ID
phonestringNoFuzzy search by phone number
status[]string[]NoMulti-status filterpending / dialing / connected / completed / failed / abandoned / blacklisted
fromdatetime ISO 8601NoFilter by created_at >= fromYYYY-MM-DD or ISO 8601
todatetime ISO 8601NoFilter by created_at <= toYYYY-MM-DD or ISO 8601
per_pageintegerNoLeads per page1-200, default 50

Bulk actions

Blacklist

http
POST /api/autocall/leads/blacklist

Body:

json
{
  "phones": ["0912345678", "0987654321"],
  "lead_ids": [78912, 78913],
  "reason": "Customer requested no further calls",
  "source": "customer_request"
}

Body fields:

FieldTypeRequiredDescriptionValid values
phonesstring[]No (one of two)Phone numbers to blacklistVietnamese format (0xxx / 84xxx)
lead_idsinteger[]No (one of two)Lead IDs — corresponding phones are auto-resolved and merged into phonesExisting lead IDs
reasonstringNoReason for blacklisting (stored in the DNC log)Up to 500 characters
sourcestringNoBlacklist sourcemanual / customer_request / duplicate / complaint / other (default manual)

Response 200:

json
{ "data": { "blacklisted": 2 } }

Response fields:

FieldTypeDescriptionValid values
data.blacklistedintegerNumber of phones inserted into the DNC (after dedup)>= 0

Unblacklist

http
POST /api/autocall/leads/unblacklist
{ "phones": ["0912345678"] }

Body fields:

FieldTypeRequiredDescriptionValid values
phonesstring[]YesPhones to remove from the DNCVietnamese format

Response 200: {"data": {"unblacklisted": 1}}

Response fields:

FieldTypeDescriptionValid values
data.unblacklistedintegerNumber of phones removed from the DNC>= 0

Bulk re-render audio

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

FieldTypeRequiredDescriptionValid values
lead_idsinteger[]YesLead IDs whose audio should be re-renderedExisting lead IDs in the account

Response 200: {"data": {"dispatched": 2}}

Response fields:

FieldTypeDescriptionValid values
data.dispatchedintegerNumber of render jobs queued>= 0

Do-Not-Call list

GET /api/autocall/dnc?phone=0912&source=manual

Query params:

FieldTypeRequiredDescriptionValid values
phonestringNoFuzzy search by phone number
sourcestringNoFilter by blacklist sourcemanual / customer_request / duplicate / complaint / other
pageintegerNoPage number>= 1, default 1
per_pageintegerNoItems per page1-200, default 50

Response 200:

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

FieldTypeDescriptionValid values
data[].idintegerDNC record IDPositive integer
data[].phone_numberstringPhone number in E.164 (84xxx)
data[].sourcestringBlacklist sourcemanual / customer_request / duplicate / complaint / other
data[].reasonstring | nullBlacklist reason
data[].blocked_byinteger | nullUser ID who blocked the number
data[].created_atdatetime ISO 8601Blacklist time (UTC)
current_pageintegerCurrent page>= 1
last_pageintegerLast page>= 1
per_pageintegerItems per page1-200
totalintegerTotal 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.

Variables (customer-defined TTS variables)

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"...).

List variables

http
GET /api/autocall/variables

Response 200:

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

FieldTypeDescriptionValid values
data[].idintegerVariable IDPositive integer
data[].codestringVariable key (used as a payload key when importing leads, and as in template_text)snake_case, max 64 chars, unique per account, immutable
data[].labelstringLocalized display nameUp to 255 characters
data[].data_typestringData type so TTS reads the value correctlyname / salutation_name / salutation_fullname / fullname / date / time / number / money
data[].descriptionstring | nullUser-facing descriptionUp to 2000 characters

Supported data types:

data_typeDescriptionInput exampleSpoken output
nameShort name"Nam""Nam"
salutation_nameSalutation + name"anh Nam""anh Nam"
salutation_fullnameSalutation + full name"anh Nguyễn Văn Nam"full
fullnameFull name"Nguyễn Văn Nam"full
dateDate Y-m-d"2026-06-30""the thirtieth of June, two thousand twenty-six"
timeTime H:i"14:30""fourteen thirty"
numberInteger"1234""one thousand two hundred thirty-four"
moneyVND amount"500000""five hundred thousand dong"

CRUD variables (admin only)

http
POST   /api/autocall/variables       (admin tier)
PUT    /api/autocall/variables/{id}
DELETE /api/autocall/variables/{id}

Create/update body:

json
{
  "code": "order_id",
  "label": "Order code",
  "data_type": "name",
  "description": "Format ORD-xxxxx",
  "example_value": "ORD-12345",
  "sort_order": 100,
  "is_active": true
}
FieldRequiredTypeDefaultDescription
codeYes (create)string, max 64, snake_caseVariable key, unique per account, immutable once created
labelYesstring, max 255Display name
data_typeYesenum (8 types, see table above)Data type so TTS reads the value correctly
descriptionNostring, max 2000nullDescription
example_valueNostringnullSample value used for TTS preview
sort_orderNointeger999Display order in the UI picker
is_activeNobooleantrueDisable 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:

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

json
{ "data": { "deleted": true } }

Response 422 (built-in variable):

json
{ "message": "Cannot delete a built-in variable (is_builtin=true)" }

Response 422 (create validation — e.g. duplicate code):

json
{
  "message": "The given data was invalid.",
  "errors": {
    "code": ["The code has already been taken."]
  }
}

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