Skip to content

Telesales Datasets

A Dataset is the schema unit for a Telesales campaign — it groups together:

  • Lead-phase custom fields (phase=lead) — data captured before the call (e.g. policy number, product, deal value)
  • Attempt-phase custom fields (phase=attempt) — outcome data the agent fills in during wrap-up (e.g. promise-to-pay date, payment method, notes)
  • Applicable disposition codes — the outcome catalogue this campaign uses
  • Conditional visibility — a field can be gated by disposition_in (e.g. "Commitment date" only shows when the disposition equals "Promise-to-Pay")

As of 2026-08-14, every new campaign must be linked to a Dataset. Legacy campaigns were auto-linked to each tenant's default dataset. The old /api/custom-fields endpoints for the two Telesales entities have been retired (HTTP 410 Gone) — see the Custom Fields page for details.

Access requirements

EndpointRequired scope
GET (read)telesales module — viewer or above
POST/PUT/DELETE (write)Permission manage_telesales_dataset (admin tier)
POST /api/v1/telesales/datasets/{code}/leads (push lead)Sanctum PAT with ability telesales:push

Dataset shape

Each dataset has the following top-level attributes:

FieldTypeDescription
idintegerAuto-increment
codestringUnique per tenant; used in the push-lead URL
namestringDisplay name (e.g. "FinanceCorp Life Insurance — Renewal")
descriptionstringShort summary
industry_tagstring | nullOptional enum: insurance, finance, tourism, healthcare, realestate, survey, other
is_defaultbooleanOne default dataset per tenant (auto-selected in the campaign wizard)
is_activebooleanfalse = hidden from list, cannot be used for new campaigns
fields[]arrayList of custom field definitions (details below)
dispositions[]arrayList of applicable disposition codes (N-N link to telesales_dispositions)
field_countintegerCached count
disposition_countintegerCached count

Each field object has:

FieldTypeDescription
idinteger
field_codestringUnique per dataset, snake_case (e.g. policy_number)
labelstringDisplay label
typestringEnum: text, textarea, number, date, datetime, select, multiselect, phone, email, boolean
requiredbooleanRequired on push-lead / disposition submit
optionsarray | nullFor select/multiselect types: [{value, label}, ...]
visible_whenobject | nullVisibility guard, e.g. {"disposition_in": ["RN-02", "RI-02"]}
phasestringlead or attempt
help_textstring | nullHint shown below the input
display_orderintegerPosition in the form

GET /api/v1/telesales/datasets — List datasets

Returns every dataset owned by the tenant.

http
GET /api/v1/telesales/datasets
Authorization: Bearer <api_token>
Accept: application/json

Query params:

ParamDescription
is_activetrue (default) returns only active datasets. Pass false to include archived ones.
industry_tagFilter by industry (e.g. insurance)

Response 200:

json
{
  "data": [
    {
      "id": 5,
      "code": "sample_life_renewal",
      "name": "FinanceCorp Life Insurance — Renewal",
      "description": "Premium reminder + policy reinstatement campaign",
      "industry_tag": "insurance",
      "is_default": true,
      "is_active": true,
      "field_count": 23,
      "disposition_count": 14
    },
    {
      "id": 6,
      "code": "sample_consumer_loan",
      "name": "SampleFin — Consumer Loan",
      "industry_tag": "finance",
      "is_default": false,
      "field_count": 15,
      "disposition_count": 10
    }
  ]
}

GET /api/v1/telesales/datasets/{id} — Dataset detail

Returns full metadata plus fields and dispositions.

http
GET /api/v1/telesales/datasets/5
Authorization: Bearer <api_token>

Response 200:

json
{
  "data": {
    "id": 5,
    "code": "sample_life_renewal",
    "name": "FinanceCorp Life Insurance — Renewal",
    "industry_tag": "insurance",
    "is_default": true,
    "is_active": true,
    "fields": [
      {
        "id": 101,
        "field_code": "full_name",
        "label": "Full name",
        "type": "text",
        "required": true,
        "phase": "lead",
        "display_order": 10
      },
      {
        "id": 102,
        "field_code": "policy_number",
        "label": "Policy number",
        "type": "text",
        "required": true,
        "phase": "lead",
        "help_text": "Format POL-YYYY-NNNN",
        "display_order": 20
      },
      {
        "id": 201,
        "field_code": "commitment_date",
        "label": "Commitment date",
        "type": "date",
        "required": true,
        "phase": "attempt",
        "visible_when": {
          "disposition_in": ["RN-02", "RI-02"]
        },
        "display_order": 10
      },
      {
        "id": 202,
        "field_code": "payment_method",
        "label": "Payment method",
        "type": "select",
        "options": [
          { "value": "cash",   "label": "Cash" },
          { "value": "bank",   "label": "Bank transfer" },
          { "value": "atm",    "label": "ATM card" },
          { "value": "wallet", "label": "e-Wallet" }
        ],
        "phase": "attempt",
        "visible_when": {
          "disposition_in": ["RN-01", "RI-01"]
        },
        "display_order": 20
      }
    ],
    "dispositions": [
      {
        "code": "RN-01",
        "label": "Renewal — Success",
        "category": "converted",
        "color": "#16A34A"
      },
      {
        "code": "RN-02",
        "label": "Renewal — Promise-to-Pay",
        "category": "callback",
        "color": "#F59E0B"
      }
    ]
  }
}

POST /api/v1/telesales/datasets/{id}/fields — Create field

http
POST /api/v1/telesales/datasets/5/fields
Authorization: Bearer <api_token>
Content-Type: application/json

{
  "field_code": "policy_number",
  "label": "Policy number",
  "type": "text",
  "required": true,
  "phase": "lead",
  "display_order": 20,
  "help_text": "Format POL-YYYY-NNNN"
}

Response 201 returns the created field.

Validation:

  • field_code must match ^[a-z][a-z0-9_]{0,63}$ and be unique within the dataset
  • type must be one of the supported types listed above
  • For select/multiselect, options (array of {value, label}) is required
  • phase must be lead or attempt

Error 422 on duplicate code:

json
{
  "error": "field_code_taken",
  "message": "Field code \"policy_number\" already exists in this dataset."
}

PUT /api/v1/telesales/datasets/{id}/fields/{fid} — Update field

field_code, type, and phase are immutable after creation (changing them could corrupt already-stored data). Mutable attributes: label, required, options, visible_when, help_text, display_order.

http
PUT /api/v1/telesales/datasets/5/fields/102
Content-Type: application/json

{
  "label": "Policy No. (Contract ID)",
  "required": false,
  "help_text": "May be empty if not yet known"
}

DELETE /api/v1/telesales/datasets/{id}/fields/{fid} — Delete field

Soft delete: is_active=false. Data stored under the custom_fields JSON column on leads/attempts is preserved. You can restore with PUT ... {"is_active": true}.

POST /api/v1/telesales/datasets/{id}/fields/reorder — Reorder fields

http
POST /api/v1/telesales/datasets/5/fields/reorder
Content-Type: application/json

{
  "items": [
    { "id": 101, "display_order": 10 },
    { "id": 102, "display_order": 20 },
    { "id": 103, "display_order": 30 }
  ]
}

GET /api/v1/telesales/datasets/{id}/dispositions — Applicable dispositions

Returns the disposition codes assigned to the dataset (a subset of the tenant-wide telesales_dispositions catalogue).

PUT /api/v1/telesales/datasets/{id}/dispositions — Set applicable dispositions

http
PUT /api/v1/telesales/datasets/5/dispositions
Content-Type: application/json

{
  "dispositions": [
    "RN-01", "RN-02", "RN-03", "RN-04",
    "RI-01", "RI-02", "RI-03",
    "CB-01", "DO-01", "OT-01"
  ]
}

POST /api/v1/telesales/datasets/{dataset_code}/leads — Push lead from an external system

Used by partners to push leads from a CRM or internal system into Zorio. Auth via Sanctum PAT with the 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
  }
}

The dataset block in the response lets clients confirm which dataset was applied — useful if the URL dataset_code was accidentally misspelled but resolved by fuzzy matching (not currently supported, kept for forward compatibility).

Validation

  • Every lead must include phone (required). Auto-normalizes 0xxx84xxx.
  • If provided, lead_data must match the dataset's phase=lead schema:
    • Field not defined → row skipped with errors[i].error = "unknown_field"
    • Required field missing → row skipped with errors[i].error = "missing_required"
    • Wrong data type → row skipped with errors[i].error = "invalid_type"
  • Phone duplicate within the same campaign → skipped_duplicate++
  • Phone in the DNC list → skipped_dnc++
  • Batch limit: 500 leads per request. Split larger batches across multiple requests.

Rate limit

  • 60 requests/minute/token
  • Exceeding the quota returns HTTP 429 with a Retry-After header

Validation pipeline for attempt-phase custom_fields

When an agent submits a disposition through POST /api/telesales/calls/{uuid}/disposition, the custom_fields payload is validated against the campaign's dataset schema (fields where phase='attempt'). The 5-step pipeline:

  1. Reject unknown fields — keys not in the dataset return 422 unknown_field
  2. Coerce data types"42" → int; "true" → bool
  3. Apply validation rules — required, min/max, options membership
  4. Filter by visible_when — fields that don't match the submitted disposition are silently dropped
  5. Persist into telesales_call_attempts.custom_fields JSON

Response 200 includes a custom_fields_report:

json
{
  "data": { "attempt_id": 999 },
  "custom_fields_report": {
    "summary": {
      "total_fields": 3,
      "accepted": 2,
      "rejected": 1,
      "filtered_by_visible_when": 0
    },
    "errors": [
      {
        "field": "commitment_date",
        "error": "missing_required",
        "message": "Field \"Commitment date\" is required when disposition = RN-02"
      }
    ]
  }
}

Migrating from the legacy Custom Fields API

If your integration used to call /api/custom-fields for the two Telesales entities, see the full migration guide on the Custom Fields page.

Quick steps:

  1. Call GET /api/v1/telesales/datasets → cache the tenant's dataset list
  2. For each relevant dataset, call GET /api/v1/telesales/datasets/{id} → cache its fields and dispositions
  3. Replace every GET /api/custom-fields?entity=TelesalesLead with reading fields[] from the dataset (filter phase='lead')
  4. Replace every POST/PUT/DELETE /api/custom-fields with the corresponding dataset endpoint
  5. When pushing leads through the new endpoint, use the dataset block in the response as confirmation

Sample code

Python (requests)

python
import requests

BASE = "https://api.zorio.vn"
TOKEN = "your_api_token"
headers = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}

# List datasets
datasets = requests.get(f"{BASE}/api/v1/telesales/datasets", headers=headers).json()

# Get schema
dataset = requests.get(f"{BASE}/api/v1/telesales/datasets/5", headers=headers).json()['data']
lead_fields = [f for f in dataset['fields'] if f['phase'] == 'lead']

# Push lead
result = requests.post(
    f"{BASE}/api/v1/telesales/datasets/sample_life_renewal/leads",
    headers=headers,
    json={
        "leads": [{"full_name": "Alice", "phone": "0900000001",
                   "lead_data": {"policy_number": "POL-2024-0001"}}],
        "campaign_code": "SAMPLE_RENEWAL_202608"
    }
).json()
print(f"Inserted {result['inserted']} into dataset {result['dataset']['name']}")

Node.js (axios)

javascript
const axios = require('axios');
const client = axios.create({
  baseURL: 'https://api.zorio.vn',
  headers: { Authorization: `Bearer ${process.env.ZORIO_TOKEN}` },
});

// Push a batch of leads
const { data } = await client.post('/api/v1/telesales/datasets/sample_life_renewal/leads', {
  leads: [
    { full_name: 'Alice', phone: '0900000001',
      lead_data: { policy_number: 'POL-2024-0001' } },
  ],
  campaign_code: 'SAMPLE_RENEWAL_202608',
});
console.log(`Inserted ${data.inserted} into dataset ${data.dataset.name}`);

cURL

bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "leads": [
         { "full_name": "Alice", "phone": "0900000001",
           "lead_data": { "policy_number": "POL-2024-0001" } }
       ],
       "campaign_code": "SAMPLE_RENEWAL_202608"
     }' \
     https://api.zorio.vn/api/v1/telesales/datasets/sample_life_renewal/leads

FAQ

Q1. Do datasets support nesting (parent-child)?
A: Not yet. Each dataset is independent. Common fields must currently be duplicated across datasets.

Q2. Can I switch the dataset for a campaign that's already running?
A: Not recommended. Leads imported under the old schema will mismatch the new one. Create a new campaign with a different dataset instead.

Q3. Can I delete a dataset that a campaign uses?
A: No. The endpoint returns 422 dataset_in_use. Delete/archive the referring campaigns first.

Q4. Is bulk create/update supported for fields?
A: Not yet. Loop through individual calls. Batch support is planned for a future release.

Q5. Which webhook fires when a lead is pushed via the dataset endpoint?
A: telesales.lead.imported — one event per successful insert. Subscribe under Portal → Webhooks.

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