Skip to content

Bộ dữ liệu Telesales (Datasets)

Bộ dữ liệu (Dataset) là đơn vị cấu hình schema cho một chiến dịch Telesales — chứa:

  • Trường tuỳ chỉnh cho lead (phase=lead) — dữ liệu nhập trước khi gọi (VD số hợp đồng, sản phẩm, giá trị đơn hàng)
  • Trường tuỳ chỉnh cho cuộc gọi (phase=attempt) — kết quả agent điền lúc wrap-up (VD ngày cam kết, phương thức thanh toán, ghi chú)
  • Danh sách mã kết quả (dispositions) áp dụng cho chiến dịch dùng bộ dữ liệu này
  • Hiển thị conditional — field chỉ hiện khi mã kết quả nằm trong danh sách disposition_in (VD "Ngày cam kết" chỉ hiện khi disposition = "Promise-to-Pay")

Từ 14/08/2026, mọi chiến dịch mới bắt buộc gắn với 1 bộ dữ liệu. Chiến dịch cũ được ánh xạ tự động sang bộ dữ liệu mặc định của tenant. API /api/custom-fields cho 2 entity TelesalesLead + TelesalesCallAttempt đã ngừng phục vụ (410 Gone) — xem trang Custom Fields để biết chi tiết.

Quyền truy cập

EndpointQuyền yêu cầu
GET (đọc)Module telesales cấp viewer trở lên
POST/PUT/DELETE (ghi)Permission manage_telesales_dataset (tier admin)
POST /api/v1/telesales/datasets/{code}/leads (push lead)Sanctum PAT với ability telesales:push

Cấu trúc bộ dữ liệu

Mỗi bộ dữ liệu có cấu trúc:

TrườngKiểuMô tả
idintegerAuto-increment
codestringUnique per tenant, dùng trong URL push lead
namestringTên hiển thị (VD "FinanceCorp Life Insurance — Renewal")
descriptionstringMô tả ngắn
industry_tagstring | nullEnum tuỳ chọn: insurance, finance, tourism, healthcare, realestate, survey, other
is_defaultboolean1 bộ mặc định per tenant (auto-select trong wizard tạo chiến dịch)
is_activebooleanfalse = ẩn khỏi list, không dùng được cho chiến dịch mới
fields[]arrayDanh sách trường tuỳ chỉnh (chi tiết dưới)
dispositions[]arrayDanh sách mã kết quả áp dụng (N-N link với telesales_dispositions)
field_countintegerCache count
disposition_countintegerCache count

Mỗi trường (field) có cấu trúc:

TrườngKiểuMô tả
idinteger
field_codestringUnique per dataset, snake_case (VD policy_number)
labelstringNhãn hiển thị
typestringEnum: text, textarea, number, date, datetime, select, multiselect, phone, email, boolean
requiredbooleanBắt buộc khi push lead / submit disposition
optionsarray | nullCho type select/multiselect: [{value, label}, ...]
visible_whenobject | nullĐiều kiện hiển thị, VD {"disposition_in": ["RN-02", "RI-02"]}
phasestringlead hoặc attempt
help_textstring | nullGợi ý hiển thị dưới input
display_orderintegerThứ tự trong form

GET /api/v1/telesales/datasets — Danh sách bộ dữ liệu

Trả về tất cả bộ dữ liệu của tenant.

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

Query params:

ParamMô tả
is_activetrue (mặc định) chỉ trả bộ active. false để bao gồm bộ đã lưu trữ.
industry_tagFilter theo ngành (VD insurance)

Response 200:

json
{
  "data": [
    {
      "id": 5,
      "code": "sample_life_renewal",
      "name": "FinanceCorp Life Insurance — Renewal",
      "description": "Chiến dịch nhắc phí + tái tục hợp đồng bảo hiểm",
      "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} — Chi tiết bộ dữ liệu

Trả về full metadata + fields + 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",
    "description": "...",
    "industry_tag": "insurance",
    "is_default": true,
    "is_active": true,
    "fields": [
      {
        "id": 101,
        "field_code": "full_name",
        "label": "Họ tên",
        "type": "text",
        "required": true,
        "phase": "lead",
        "display_order": 10
      },
      {
        "id": 102,
        "field_code": "policy_number",
        "label": "Số hợp đồng",
        "type": "text",
        "required": true,
        "phase": "lead",
        "help_text": "Định dạng POL-YYYY-NNNN",
        "display_order": 20
      },
      {
        "id": 103,
        "field_code": "annual_premium",
        "label": "Phí bảo hiểm hàng năm",
        "type": "number",
        "required": false,
        "phase": "lead",
        "display_order": 30
      },
      {
        "id": 201,
        "field_code": "commitment_date",
        "label": "Ngày cam kết thanh toán",
        "type": "date",
        "required": true,
        "phase": "attempt",
        "visible_when": {
          "disposition_in": ["RN-02", "RI-02"]
        },
        "display_order": 10
      },
      {
        "id": 202,
        "field_code": "payment_method",
        "label": "Phương thức thanh toán",
        "type": "select",
        "options": [
          { "value": "cash",     "label": "Tiền mặt" },
          { "value": "bank",     "label": "Chuyển khoản" },
          { "value": "atm",      "label": "Thẻ ATM" },
          { "value": "wallet",   "label": "Ví điện tử" }
        ],
        "phase": "attempt",
        "visible_when": {
          "disposition_in": ["RN-01", "RI-01"]
        },
        "display_order": 20
      }
    ],
    "dispositions": [
      {
        "code": "RN-01",
        "label": "Renewal — Thành công",
        "category": "converted",
        "color": "#16A34A"
      },
      {
        "code": "RN-02",
        "label": "Renewal — Hẹn thanh toán",
        "category": "callback",
        "color": "#F59E0B"
      }
    ]
  }
}

POST /api/v1/telesales/datasets/{id}/fields — Tạo trường mới

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

{
  "field_code": "policy_number",
  "label": "Số hợp đồng",
  "type": "text",
  "required": true,
  "phase": "lead",
  "display_order": 20,
  "help_text": "Định dạng POL-YYYY-NNNN"
}

Response 201 trả về field vừa tạo:

json
{
  "data": {
    "id": 102,
    "dataset_id": 5,
    "field_code": "policy_number",
    "label": "Số hợp đồng",
    "type": "text",
    "required": true,
    "phase": "lead",
    "display_order": 20
  }
}

Validation:

  • field_code phải khớp regex ^[a-z][a-z0-9_]{0,63}$ và unique trong dataset
  • type phải nằm trong danh sách hỗ trợ
  • Với type select/multiselect, bắt buộc có options (array of {value, label})
  • phase bắt buộc lead hoặc attempt

Error 422 nếu trùng code:

json
{
  "error": "field_code_taken",
  "message": "Mã trường \"policy_number\" đã tồn tại trong bộ dữ liệu này."
}

PUT /api/v1/telesales/datasets/{id}/fields/{fid} — Sửa trường

Không cho phép đổi field_code, type, phase sau khi tạo (data đã lưu có thể bị corrupt). Cho phép đổi label, required, options, visible_when, help_text, display_order.

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

{
  "label": "Mã hợp đồng (Policy No.)",
  "required": false,
  "help_text": "Có thể để trống nếu chưa có"
}

DELETE /api/v1/telesales/datasets/{id}/fields/{fid} — Xoá trường

Soft delete: is_active=false. Data đã lưu trong custom_fields JSON của leads/attempts vẫn giữ. Có thể restore bằng PUT ... {"is_active": true}.

POST /api/v1/telesales/datasets/{id}/fields/reorder — Sắp xếp lại thứ tự

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 — Danh sách mã kết quả áp dụng

http
GET /api/v1/telesales/datasets/5/dispositions

Response: array các mã kết quả đã gán cho bộ dữ liệu (subset của catalog telesales_dispositions chung của tenant).

PUT /api/v1/telesales/datasets/{id}/dispositions — Cập nhật mã kết quả áp dụng

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 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ạ, phòng trường hợp dataset_code sai chính tả trong URL nhưng vẫn được match fuzzy (hiện chưa hỗ trợ nhưng để tương thích ngược).

Validation

  • Mỗi lead phải có phone (bắt buộc). Auto normalize 0xxx84xxx.
  • 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

Pipeline kiểm tra khi ghi custom_fields (phase=attempt)

Khi agent submit disposition qua POST /api/telesales/calls/{uuid}/disposition, giá trị custom_fields được validate theo schema phase=attempt của dataset của chiến dịch. Pipeline 5 bước:

  1. Từ chối field không có trong dataset — key không match → 422 unknown_field
  2. Chuyển đổi kiểu dữ liệu"42" → int; "true" → bool
  3. Áp dụng validation — required, min/max, options membership
  4. Filter theo visible_when — field không match disposition đang gửi → bị bỏ qua (không lưu)
  5. Persist vào telesales_call_attempts.custom_fields JSON

Response 200 kèm 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": "Trường \"Ngày cam kết thanh toán\" bắt buộc khi disposition = RN-02"
      }
    ]
  }
}

Migration từ Custom Fields cũ

Nếu integration của bạn trước đây gọi /api/custom-fields cho 2 entity Telesales, xem hướng dẫn migration đầy đủ tại trang Custom Fields.

Tóm tắt các bước:

  1. Gọi GET /api/v1/telesales/datasets → cache list bộ dữ liệu của tenant
  2. Với mỗi bộ dữ liệu quan tâm, gọi GET /api/v1/telesales/datasets/{id} → cache fields + dispositions
  3. Thay mọi call GET /api/custom-fields?entity=TelesalesLead bằng đọc fields[] từ dataset (filter phase='lead')
  4. Thay mọi call POST/PUT/DELETE /api/custom-fields bằng endpoint dataset tương ứng
  5. Khi push lead qua endpoint mới, dùng response dataset block để confirm

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 lead batch
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. Dataset có nested (parent-child) không?
A: Chưa. Mỗi dataset độc lập. Nếu muốn share field common, hiện phải copy sang từng dataset.

Q2. Chuyển dataset cho chiến dịch đang chạy được không?
A: Không khuyến nghị. Lead đã import theo schema cũ → data sẽ mismatch. Nên tạo chiến dịch mới với dataset khác.

Q3. Xoá dataset đang dùng cho chiến dịch được không?
A: Không. Trả 422 dataset_in_use. Phải xoá/lưu trữ chiến dịch trước.

Q4. Bulk create/update field không?
A: Chưa hỗ trợ. Loop từng call. Sẽ bổ sung trong bản v1.18+.

Q5. Webhook nào fire khi lead push qua dataset endpoint?
A: telesales.lead.imported — fire per-lead sau khi INSERT thành công. Subscribe trong Portal → Webhooks.

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