# Zorio Developer Portal > Tài liệu chính thức tích hợp REST API + Webphone SDK của hệ thống Zorio Contact Center. Generated for AI agents. Source: https://docs.zorio.vn --- # Campaigns # Campaigns Quản lý chiến dịch AutoCall — CRUD + lifecycle controls (launch / pause / resume / archive / unarchive) + thống kê. ## Tạo chiến dịch ```http POST /api/autocall/campaigns ``` **Body**: ```json { "name": "Chăm sóc khách hàng Q3-2026", "description": "Chăm sóc khách hàng kích hoạt lại sau 30-60 ngày", "script_id": 12, "caller_id_group_id": 3, "max_retries": 3, "retry_interval_minutes": 60, "retry_only_when": "no_answer", "max_concurrent": 10, "originate_timeout": 40, "write_to_main_cdr": false, "start_at": "2026-06-25T08:00:00Z", "end_at": "2026-06-30T17:30:00Z" } ``` **Fields**: | Field | Required | Type | Mô tả | |---|---|---|---| | `name` | Có | string, max:255 | Tên chiến dịch | | `description` | Không | string, max:2000 | Mô tả | | `script_id` | Có | integer | ID script TTS (xem [Scripts](/api/autocall/scripts)). PHẢI tồn tại **VÀ** `status='active'`. Truyền script đang `draft`/`archived` -> **422**. | | `caller_id_group_id` | Không | integer | ID pool đầu số. Bỏ trống -> engine fallback default trunk của khách hàng. PHẢI tồn tại + `status='active'`. | | `max_retries` | Không | integer (0-10, default 3) | Số lần thử lại tối đa nếu attempt fail | | `retry_interval_minutes` | Không | integer (1-43200, default 60) | Khoảng cách giữa các attempt (phút) | | `retry_only_when` | Không | enum (`no_answer`/`busy`/`failed`/`any`, default `no_answer`) | Chỉ retry khi attempt result match | | `max_concurrent` | Không | integer (1-200, default 10) | Số cuộc đồng thời tối đa | | `originate_timeout` | Không | integer (10-180, default 40) | Timeout chờ B-leg nhận máy (giây) | | `write_to_main_cdr` | Không | boolean (default false) | Có ghi vào CDR chung hệ thống không. False = chỉ ghi log nội bộ AutoCall. | | `start_at` | Không | datetime UTC | Thời điểm bắt đầu được dial. Bỏ trống = launch ngay | | `end_at` | Không | datetime UTC | Thời điểm dừng (phải > start_at) | **Response 201** (response trả đầy đủ các field): ```json { "data": { "id": 19, "tenant_id": 1, "name": "Chăm sóc khách hàng Q3-2026", "description": "Chăm sóc khách hàng kích hoạt lại sau 30-60 ngày", "script_id": 12, "status": "draft", "gateway_name": null, "caller_id": null, "caller_id_group_id": 8, "max_retries": 3, "retry_interval_minutes": 60, "retry_only_when": "no_answer", "write_to_main_cdr": false, "max_concurrent": 10, "originate_timeout": 40, "start_at": "2026-06-25T08:00:00.000000Z", "end_at": "2026-06-30T17:30:00.000000Z", "total_leads": 0, "total_calls": 0, "total_connected": 0, "total_failed": 0, "created_by": 3, "created_at": "2026-06-25T09:53:47.000000Z", "updated_at": "2026-06-25T09:53:47.000000Z", "deleted_at": null } } ``` ::: tip Ghi chú Response trả tất cả field cấu hình của campaign. Stats fields (`total_*`) = 0 lúc tạo (chưa có lead/call activity). Field `gateway_name`, `caller_id` (single override) null nếu dùng pool theo `caller_id_group_id`. Field `deleted_at` null nếu chưa archive (soft delete). ::: **Response 422** (validation): ```json { "message": "The given data was invalid.", "errors": { "script_id": ["The selected script id is invalid."], "retry_only_when": ["The selected retry only when is invalid."] } } ``` ## Lifecycle controls | Endpoint | Action | Từ -> Sang | |---|---|---| | `POST /api/autocall/campaigns/{id}/launch` | Kích hoạt | `draft`/`paused` -> `active` | | `POST /api/autocall/campaigns/{id}/pause` | Tạm dừng | `active` -> `paused` | | `POST /api/autocall/campaigns/{id}/resume` | Tiếp tục | `paused` -> `active` (alias của launch) | | `POST /api/autocall/campaigns/{id}/archive` | Lưu trữ | `draft`/`paused`/`done` -> `archived` (yêu cầu KHÔNG đang active) | | `POST /api/autocall/campaigns/{id}/unarchive` | Khôi phục | `archived` -> `paused` | | `PUT /api/autocall/campaigns/{id}` | Cập nhật metadata | giữ status | | `DELETE /api/autocall/campaigns/{id}` | Xoá VĨNH VIỄN | yêu cầu `archived` trước (hard delete cascade: campaign + leads + attempts + dtmf_events) | ::: warning Quy tắc bắt buộc - Phải `pause` trước khi `archive` (campaign đang chạy không thể archive). - Phải `archive` trước khi `delete` (xoá vĩnh viễn -> mất data báo cáo). ::: **Response 200** (áp dụng cho mọi action launch/pause/resume/archive/unarchive): ```json { "data": { "id": 21, "tenant_id": 1, "name": "Chăm sóc Q3-2026 2", "description": "Chăm sóc khách hàng kích hoạt lại sau 30-60 ngày", "script_id": 12, "status": "active", "gateway_name": null, "caller_id": null, "caller_id_group_id": 8, "max_retries": 3, "retry_interval_minutes": 60, "retry_only_when": "no_answer", "write_to_main_cdr": false, "max_concurrent": 10, "originate_timeout": 40, "start_at": "2026-06-25T08:00:00.000000Z", "end_at": "2026-06-30T17:30:00.000000Z", "total_leads": 0, "total_calls": 0, "total_connected": 0, "total_failed": 0, "created_by": 3, "created_at": "2026-06-25T14:26:16.000000Z", "updated_at": "2026-06-25T14:27:41.000000Z", "deleted_at": null } } ``` **Field bổ sung** (không có ở body POST): | Field | Type | Mô tả | |---|---|---| | `gateway_name` | string \| null | Override gateway dial cụ thể (mặc định null = dùng default routing) | | `caller_id` | string \| null | Override caller ID đơn (mặc định null = dùng pool theo `caller_id_group_id`) | | `total_leads` | integer | Tổng lead đã import vào campaign | | `total_calls` | integer | Tổng cuộc gọi đã dial | | `total_connected` | integer | Tổng cuộc connect thành công | | `total_failed` | integer | Tổng cuộc fail | | `deleted_at` | timestamp \| null | Soft delete timestamp (null nếu chưa archive) | ::: tip Timestamps format ISO 8601 với **microseconds** `.000000Z`. Khách hàng parse phải hỗ trợ format này (vd `new Date("2026-06-25T14:26:16.000000Z")` trong JS hoạt động đúng). ::: **Response 422** (state machine fail): ```json { "message": "Chỉ campaign draft/paused mới launch được" } { "message": "Chỉ campaign active mới pause được" } { "message": "Phải tạm dừng chiến dịch trước khi lưu trữ" } { "message": "Chỉ chiến dịch đã lưu trữ mới có thể khôi phục" } { "message": "Phải lưu trữ chiến dịch trước khi xoá vĩnh viễn (Lưu trữ -> Xoá)" } ``` **Response 200** (`PUT /api/autocall/campaigns/{id}` — cập nhật metadata): trả campaign object đã update, shape giống Response 201. **Response 200** (`DELETE /api/autocall/campaigns/{id}`): ```json { "data": { "deleted": true } } ``` ## List / show details / stats ### `GET /api/autocall/campaigns?status=active` **Query params**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `status` | string | Không | Lọc theo trạng thái chiến dịch | `draft` / `active` / `paused` / `done` / `archived` | | `archived` | integer | Không | Lấy danh sách chiến dịch đã lưu trữ. Mặc định ẨN archived | `1` = chỉ archived; bỏ trống = không archived | | `q` | string | Không | Tìm kiếm gần đúng theo tên chiến dịch (LIKE `%keyword%`) | Chuỗi text bất kỳ | **Response 200**: ```json { "data": [ { "id": 36, "name": "Chăm sóc khách hàng Q3-2026", "status": "active", "script": { "id": 12, "name": "Chăm sóc chuẩn", "voice_id": "evln-vi-nature" }, "total_leads": 487, "total_calls": 312, "total_connected": 198, "total_failed": 89, "created_at": "2026-06-24T07:30:00Z" } ] } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data[].id` | integer | ID chiến dịch | Số nguyên dương | | `data[].name` | string | Tên chiến dịch | Tối đa 255 ký tự | | `data[].status` | string | Trạng thái lifecycle hiện tại | `draft` / `active` / `paused` / `done` / `archived` | | `data[].script` | object | Script TTS đính kèm (eager-load) | Object con với `id`, `name`, `voice_id` | | `data[].script.id` | integer | ID script | Số nguyên dương | | `data[].script.name` | string | Tên script | — | | `data[].script.voice_id` | string | ID giọng đọc TTS được script dùng | — | | `data[].total_leads` | integer | Tổng lead đã import vào chiến dịch | >= 0 | | `data[].total_calls` | integer | Tổng cuộc gọi đã dial | >= 0 | | `data[].total_connected` | integer | Tổng cuộc gọi connect thành công (B-leg answer) | >= 0 | | `data[].total_failed` | integer | Tổng cuộc gọi thất bại | >= 0 | | `data[].created_at` | datetime ISO 8601 | Thời điểm tạo chiến dịch (UTC) | — | ### `GET /api/autocall/campaigns/{id}` Trả về chi tiết đầy đủ kèm `script.dtmfActions` và `agents`. **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 tồn tại trong tài khoản | **Response 200**: ```json { "data": { "id": 36, "name": "Chăm sóc khách hàng Q3-2026", "description": "Chăm sóc khách hàng kích hoạt lại sau 30-60 ngày", "status": "active", "script_id": 12, "caller_id_group_id": 3, "max_retries": 3, "retry_interval_minutes": 60, "retry_only_when": "no_answer", "max_concurrent": 10, "originate_timeout": 40, "write_to_main_cdr": false, "start_at": "2026-06-25T08:00:00Z", "end_at": "2026-06-30T17:30:00Z", "total_leads": 487, "total_calls": 312, "total_connected": 198, "total_failed": 89, "tenant_id": 1, "created_by": 7, "created_at": "2026-06-24T07:30:00Z", "updated_at": "2026-06-25T02:34:00Z", "script": { "id": 12, "name": "Chăm sóc chuẩn", "voice_id": "evln-vi-nature", "dtmf_actions": [ { "id": 45, "digit": "1", "label": "Xác nhận", "action_type": "playback_then_hangup", "target_audio_url": "/media/thanks.wav" }, { "id": 46, "digit": "2", "label": "Chuyển NV", "action_type": "queue", "target_queue_name": "support_queue" } ] }, "agents": [] } } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data.id` | integer | ID chiến dịch | Số nguyên dương | | `data.name` | string | Tên chiến dịch | — | | `data.description` | string \| null | Mô tả | — | | `data.status` | string | Trạng thái lifecycle | `draft` / `active` / `paused` / `done` / `archived` | | `data.script_id` | integer | ID script TTS đang dùng | — | | `data.caller_id_group_id` | integer \| null | ID pool đầu số rotation | — | | `data.max_retries` | integer | Số lần thử lại tối đa nếu attempt fail | 0-10 | | `data.retry_interval_minutes` | integer | Khoảng cách giữa các attempt (phút) | 1-43200 | | `data.retry_only_when` | string | Điều kiện chỉ retry khi result match | `no_answer` / `busy` / `failed` / `any` | | `data.max_concurrent` | integer | Số cuộc đồng thời tối đa | 1-200 | | `data.originate_timeout` | integer | Timeout chờ B-leg nhận máy (giây) | 10-180 | | `data.write_to_main_cdr` | boolean | Có ghi CDR chung hệ thống không | `true` / `false` | | `data.start_at` | datetime ISO 8601 \| null | Thời điểm bắt đầu được dial (UTC) | — | | `data.end_at` | datetime ISO 8601 \| null | Thời điểm dừng dial (UTC) | — | | `data.total_leads` | integer | Tổng lead đã import | >= 0 | | `data.total_calls` | integer | Tổng cuộc đã dial | >= 0 | | `data.total_connected` | integer | Tổng cuộc connect thành công | >= 0 | | `data.total_failed` | integer | Tổng cuộc fail | >= 0 | | `data.tenant_id` | integer | ID khách hàng sở hữu | — | | `data.created_by` | integer | User ID người tạo | — | | `data.created_at` | datetime ISO 8601 | Thời điểm tạo (UTC) | — | | `data.updated_at` | datetime ISO 8601 | Thời điểm cập nhật gần nhất (UTC) | — | | `data.script` | object | Script TTS đính kèm + danh sách phím DTMF | — | | `data.script.dtmf_actions[]` | array | Cấu hình các phím bấm DTMF của script (xem [Scripts](/api/autocall/scripts)) | — | | `data.agents[]` | array | Danh sách agent gán riêng vào chiến dịch (rỗng nếu không gán) | — | **Response 404** (campaign không tồn tại hoặc thuộc khách hàng khác): ```json { "message": "Campaign không tồn tại" } ``` ### `GET /api/autocall/campaigns/{id}/stats` **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 | **Response 200**: ```json { "data": { "total_leads": 487, "total_calls": 312, "total_connected": 198, "total_failed": 89, "total_no_answer": 65, "total_busy": 18, "connection_rate": 63.5 } } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data.total_leads` | integer | Tổng lead đã import vào chiến dịch | >= 0 | | `data.total_calls` | integer | Tổng cuộc đã dial | >= 0 | | `data.total_connected` | integer | Tổng cuộc connect thành công (B-leg answer) | >= 0 | | `data.total_failed` | integer | Tổng cuộc fail (đếm gộp `failed` + `no_answer` + `busy`) | >= 0 | | `data.total_no_answer` | integer | Tổng cuộc không bắt máy | >= 0 | | `data.total_busy` | integer | Tổng cuộc máy bận | >= 0 | | `data.connection_rate` | number | Tỷ lệ kết nối thành công (`total_connected / total_calls * 100`) | 0-100, làm tròn 1 chữ số thập phân | ### `GET /api/autocall/campaigns/{id}/attempts?per_page=50` Danh sách cuộc gọi của campaign (phân trang chuẩn `page`/`per_page`, default 50, max 200). **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ệ | |---|---|---|---|---| | `page` | integer | Không | Trang phân trang (1-indexed) | >= 1, default `1` | | `per_page` | integer | Không | Số attempt mỗi trang | 1-200, default `50` | **Response 200**: ```json { "data": [ { "id": 8721, "uuid": "093e1024-1234-5678-9abc-def012345678", "campaign_id": 36, "lead_id": 78912, "lead_phone_number": "0912345678", "attempt_number": 1, "started_at": "2026-06-24T08:32:00Z", "answered_at": "2026-06-24T08:32:05Z", "ended_at": "2026-06-24T08:32:47Z", "duration_sec": 42, "result": "connected", "hangup_cause": "NORMAL_CLEARING", "dtmf_pressed": "2", "final_action": "queue:support_queue" } ], "current_page": 1, "last_page": 7, "per_page": 50, "total": 312 } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data[].id` | integer | ID attempt | Số nguyên dương | | `data[].uuid` | string | UUID cuộc gọi (dùng để hangup hoặc tra cứu) | UUID v4 | | `data[].campaign_id` | integer | ID chiến dịch | — | | `data[].lead_id` | integer | ID lead | — | | `data[].lead_phone_number` | string | Số điện thoại lead (format E.164 `84xxx`) | — | | `data[].attempt_number` | integer | Lần thử thứ mấy | 1-N | | `data[].started_at` | datetime ISO 8601 | Thời điểm bắt đầu originate (UTC) | — | | `data[].answered_at` | datetime ISO 8601 \| null | Thời điểm B-leg answer (UTC) | null nếu không answer | | `data[].ended_at` | datetime ISO 8601 | Thời điểm cuộc gọi kết thúc (UTC) | — | | `data[].duration_sec` | integer | Tổng thời lượng cuộc gọi (giây) | >= 0 | | `data[].result` | string | Kết quả cuộc gọi | `connected` / `no_answer` / `busy` / `failed` / `abandoned` | | `data[].hangup_cause` | string | Mã hangup từ Zorio PBX | `NORMAL_CLEARING` / `NO_ANSWER` / `USER_BUSY` / ... | | `data[].dtmf_pressed` | string \| null | Phím DTMF khách hàng bấm (nếu có) | `0`-`9`, `*`, `#`, null | | `data[].final_action` | string \| null | Action cuối cùng đã thực thi | `queue:` / `playback:` / `hangup` / `switch:` | | `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ố attempt khớp filter | >= 0 | ## Caller-ID Groups (read-only picker) ### `GET /api/autocall/caller-id-pools` Lấy danh sách pool đầu số để dùng làm `caller_id_group_id` khi tạo campaign. **Response 200**: ```json { "data": [ { "id": 3, "name": "Pool VNPT 028", "label": "Pool VNPT 028", "description": "5 số 028xxxx, rotation Round Robin" } ] } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data[].id` | integer | ID pool — dùng làm `caller_id_group_id` khi `POST /api/autocall/campaigns` | Số nguyên dương | | `data[].name` | string | Tên pool (định danh kỹ thuật) | Tối đa 255 ký tự | | `data[].label` | string | Tên hiển thị friendly cho UI | Tối đa 255 ký tự | | `data[].description` | string \| null | Mô tả ngắn về pool (cách rotation, danh sách số...) | — | ## Hangup cuộc gọi đang chạy Hangup cuộc gọi đang chạy cho campaign AutoCall — AutoCall và Telesales chia sẻ endpoint hangup chung. ```http POST /api/telesales/calls/{uuid}/hangup ``` **Path params**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `uuid` | string | Có | UUID cuộc gọi (lấy từ webhook `call.answered` event hoặc field `uuid` trong attempt) | UUID v4 thuộc tài khoản hiện tại | **Request mẫu**: ```bash curl -X POST https://app.zorio.vn/api/telesales/calls/abc-def-123/hangup \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" ``` **Response 200**: ```json { "success": true } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `success` | boolean | Đã gửi lệnh hangup tới Zorio PBX thành công | `true` | **Error 404** (uuid không tồn tại hoặc không thuộc tài khoản): ```json { "error": "Cuộc gọi không tồn tại" } ``` ::: warning Permission Cần `manage_telesales_campaigns` hoặc role `admin`. Cuộc gọi sẽ kết thúc với hangup cause `NORMAL_CLEARING`. ::: --- # AutoCall TTS API — Tổng quan # AutoCall TTS API — Tổng quan > Public REST API cho Module AutoCall TTS — tự động hoá outbound với giọng đọc tự nhiên + DTMF actions + voices library. > > **Xác thực**: Bearer Token (phiên user). **Base URL**: `https://app.zorio.vn/api/autocall/` (cấp riêng theo khách hàng). ## Phạm vi AutoCall API gồm các nhóm chính: - Quản lý chiến dịch (Campaigns). - Script + DTMF actions (playback / playback_then_hangup / switch_script / queue / repeat_script / hangup). - Leads + Variables (placeholder `{{xxx}}` trong template). - Voices + Media files (tier A premium realtime, tier B local pre-rendered). - Reports — KPIs / Trend / Comparison / Hangup / Heatmap / DTMF / Funnel / Export. - Webhook events. ## Tier A/B - **Tier A**: premium realtime — TTS qua provider (nhà cung cấp TTS cao cấp) tính phí theo ký tự, khách hàng cấp key. - **Tier B**: local unlimited — dùng audio library vi-VN-female-01 pre-rendered, không phí. Khuyến nghị default cho volume lớn. ## Glossary | Thuật ngữ | Giải thích | |---|---| | Campaign | Chiến dịch outbound TTS gắn 1 script + danh sách lead + cấu hình caller-ID + tốc độ dial | | Script | Kịch bản TTS gồm `template_text` chứa biến `{{placeholder}}` + cây DTMF action | | DTMF | Mã phím khách bấm sau khi nghe TTS (0-9, *, #) — Zorio capture để branching | | DTMF action | Hành động khi khách bấm phím — `playback` / `playback_then_hangup` / `switch_script` / `queue` / `repeat_script` / `hangup` | | Lead | 1 số điện thoại cần gọi + payload biến TTS tương ứng | | Variable | Biến do khách hàng định nghĩa, đại diện cho data động trong script (vd `customer_name`, `appointment_date`, `callback_date`) | | Voice | Giọng đọc cụ thể (vd "vi-VN-female-01" Tier B, "Roger" Tier A nhà cung cấp TTS cao cấp) | | Media file | File audio upload sẵn để playback (không qua TTS) — dùng cho intro fixed, music on hold, IVR fallback | | Tier A | TTS realtime qua provider — chất lượng cao, tính phí theo char | | Tier B | Audio library pre-rendered — không phí, throughput cao, voice fix | | Attempt | 1 lần dial 1 lead — có thể retry nếu busy / no_answer | | Hangup cause | Mã chuẩn Q.850 chỉ lý do cuộc gọi kết thúc — xem [Hangup codes](/tham-chieu/hangup-codes) | ## End-to-end workflow ``` 1. Login POST /api/auth/login → Bearer token 2. Tạo script POST /api/autocall/scripts → ID + template_text + dtmf_actions 3. Tạo campaign POST /api/autocall/campaigns → bind script_id + caller_id_group_id 4. Import lead POST /api/autocall/campaigns/{id}/leads 5. Khởi chạy POST /api/autocall/campaigns/{id}/start 6. Theo dõi realtime Webhook autocall.lead.* gửi tới URL của bạn 7. Xem báo cáo GET /api/autocall/campaigns/{id}/reports/... 8. Pause / Resume / Stop POST /api/autocall/campaigns/{id}/pause | resume | stop ``` ## Xác thực Dùng Bearer Token (phiên user) — xem chi tiết [Xác thực](/auth/sanctum-bearer). ``` Authorization: Bearer Accept: application/json ``` Permission yêu cầu: `autocall_api_access` (admin role có sẵn). Một số endpoint nhạy cảm (vd start campaign) cần thêm `autocall_manage_campaign`. ## Các nhóm endpoint | Nhóm | Tài liệu | |---|---| | Campaigns | [Tới trang](/api/autocall/campaigns) | | Scripts + DTMF actions | [Tới trang](/api/autocall/scripts) | | Leads + Variables | [Tới trang](/api/autocall/leads) | | Voices + Media files | [Tới trang](/api/autocall/voices) | | Reports | [Tới trang](/api/autocall/reports) | | Webhook events AutoCall | [Tới trang](/api/autocall/webhooks) | ## Quy ước chung ### Pagination Mọi list endpoint dùng `?page=1&per_page=50` (tối đa 200). Response paginated chuẩn — xem [Quy ước chung](/bat-dau/quy-uoc). ### Response shape ```json { "data": {} } // single resource { "current_page": 1, "data": [], ... } // list paginated { "message": "...", "errors": {} } // 422 validation ``` ### Timezone ISO 8601 với offset (`2026-06-30T07:30:45+00:00`). Backend hệ thống trả với microseconds (`.000000Z`) — request chấp nhận cả hai format. ### Số điện thoại - Lưu DB ở format E.164 không dấu `+` (vd `84987654321`). - Request body chấp nhận `0xxx` hoặc `84xxx` — backend tự normalize. - Response trả format gốc client gửi lên (preserve). ## Rate limit | Endpoint group | Limit | |---|---| | `/api/autocall/*` | 120 req/phút/token | | `/api/autocall/campaigns/{id}/leads` (import bulk) | 30 req/phút/token | | `/api/autocall/campaigns/{id}/start` | 5 req/phút/token | Xem chi tiết [Rate limit + Idempotency](/auth/rate-limit). ## HTTP Status Codes | HTTP | Khi nào | |---|---| | 200 | OK | | 201 | Created (campaign / script / lead mới) | | 202 | Accepted (campaign start enqueued, audio render đang chạy) | | 401 | Token thiếu / sai / expired | | 403 | Thiếu permission `autocall_api_access` hoặc `autocall_manage_campaign` | | 404 | Resource không tồn tại trong tài khoản | | 409 | State machine vi phạm (vd start campaign đã running) | | 422 | Validation lỗi — `errors[]` chi tiết field | | 429 | Rate limit hit | | 500 | Server error | ## Base URL | | URL | |---|---| | Production | `https://app.zorio.vn/api/autocall/` (cấp riêng theo khách hàng) | ## FAQ **Q1: Voice tiếng Việt nào tốt nhất?** A: Tier B đã có **vi-VN-female-01** pre-rendered — chất lượng cao, không phí, throughput cao. Tier A có nhiều voice hơn qua provider — list qua `GET /api/autocall/voices`. **Q2: Bao nhiêu lead 1 campaign tối đa?** A: Không giới hạn cứng. Recommend ≤ **100.000 lead** per campaign để engine xử lý mượt. **Q3: Khách bấm DTMF, tôi nhận data ở đâu?** A: Subscribe webhook `autocall.lead.dtmf_pressed` — fire mỗi lần bấm phím. Payload có `digit` + `lead_id` + `script_id`. **Q4: Audio render lỗi — khắc phục thế nào?** A: Check theo thứ tự: 1. `voice_id` còn `status=active` trong `/api/autocall/voices`? 2. Provider Tier A có key valid + còn quota? 3. Tier B: `voice_id` có pre-render trong audio library? 4. Template có biến `{{xxx}}` chưa định nghĩa trong Variables? **Q5: Có thể chuyển sang agent thật khi khách bấm DTMF không?** A: Có — dùng `action_type=queue` trong DTMF action, set `target_queue_name`. Khách sẽ được transfer vào hàng đợi, agent sẽ nhận. --- # Leads + Variables # 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) ```http POST /api/autocall/campaigns/{id}/leads/import ``` **Path 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**: ```json { "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.* ::: tip 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](#variables-bien-tts)). 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ý do `invalid_phone_format` - **Payload thiếu biến required** trong `script.template_text` placeholder `{{xxx}}` -> skip -> đếm vào `skipped_invalid`, lý do `missing_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 ::: warning Validate variables vs template_text Hệ thống extract placeholder `{{xxx}}` 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): ```json { "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 | — | ::: tip 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**: ```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 } ``` **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 ```http POST /api/autocall/leads/blacklist ``` **Body**: ```json { "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**: ```json { "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 ```http 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 ```http 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**: ```json { "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 | ::: tip 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 `{{variable_code}}` theo `data_type` -> format đúng (số tiền -> đọc thành chữ, ngày -> "ngày 15 tháng 7"...). ### List variables ```http GET /api/autocall/variables ``` **Response 200**: ```json { "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 `{{code}}` 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) ```http POST /api/autocall/variables (admin tier) PUT /api/autocall/variables/{id} DELETE /api/autocall/variables/{id} ``` Body create/update: ```json { "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**: ```json { "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**: ```json { "data": { "deleted": true } } ``` **Response 422** (variable hệ thống): ```json { "message": "Không thể xoá biến hệ thống (is_builtin=true)" } ``` **Response 422** (validation create — vd `code` trùng): ```json { "message": "The given data was invalid.", "errors": { "code": ["The code has already been taken."] } } ``` --- # Reports # Reports Các endpoint báo cáo cho campaign AutoCall — KPIs, trend, comparison, hangup analysis, heatmap, DTMF analytics, funnel + export Excel. ::: tip Permission yêu cầu Mọi endpoint dưới đây cần `autocall_api_access`. Endpoint export cần thêm `autocall_export_report`. ::: ## GET `/api/autocall/campaigns/{id}/reports/kpis` — KPIs tổng quan ### Query parameters | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `from` | datetime | optional | Bắt đầu cửa sổ thống kê | ISO 8601. Default: lúc campaign start | | `to` | datetime | optional | Kết thúc cửa sổ | ISO 8601. Default: now | | `timezone` | string | optional | Múi giờ aggregate | Default: `Asia/Ho_Chi_Minh` | ### Response 200 ```json { "data": { "total_leads": 5000, "dialed": 4823, "answered": 3120, "no_answer": 1234, "busy": 287, "failed": 182, "abandoned": 0, "answer_rate_pct": "64.69", "avg_call_duration_sec": 42, "total_talk_time_sec": 131040, "dtmf_distribution": { "1": 1840, "2": 950, "0": 215, "no_input": 115 }, "transfer_to_queue_count": 1840, "transfer_success_rate_pct": "98.21", "computed_at": "2026-06-30T08:00:00+00:00" } } ``` ### Field giải thích | Field | Mô tả | |---|---| | `total_leads` | Tổng lead trong campaign | | `dialed` | Số attempt đã thực hiện (1 lead có thể nhiều attempt) | | `answered` / `no_answer` / `busy` / `failed` | Phân loại theo `result` | | `answer_rate_pct` | `answered ÷ dialed × 100` — chỉ số chính đo chất lượng pool | | `avg_call_duration_sec` | Trung bình `billsec` của cuộc answered | | `dtmf_distribution` | Phân bố phím khách bấm (key = digit, value = count) | | `transfer_to_queue_count` | Số cuộc đã transfer thành công vào queue agent | ## GET `/api/autocall/campaigns/{id}/reports/trend` — Xu hướng theo ngày ### Query parameters | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `from` | date | optional | Ngày bắt đầu | YYYY-MM-DD | | `to` | date | optional | Ngày kết thúc | YYYY-MM-DD | | `granularity` | string | optional | Độ chi tiết | `day` (default), `hour` | ### Response 200 ```json { "data": [ { "bucket": "2026-06-28", "dialed": 1200, "answered": 780, "answer_rate_pct": "65.00", "avg_duration_sec": 41, "transfer_count": 460 }, { "bucket": "2026-06-29", "dialed": 1612, "answered": 1098, "answer_rate_pct": "68.11", "avg_duration_sec": 43, "transfer_count": 642 } ] } ``` Dùng cho biểu đồ line chart theo dõi chiến dịch theo thời gian. ## GET `/api/autocall/campaigns/comparison` — So sánh nhiều campaign ::: tip Endpoint cross-campaign Khác với các endpoint khác (nested theo campaign), endpoint này nằm trực tiếp dưới `/campaigns/comparison` để so sánh N campaign cùng lúc. ::: ### Query parameters | Field | Type | Required | Mô tả | |---|---|---|---| | `campaign_ids` | string | ✅ | Danh sách ID campaign, comma-separated (vd `5,8,12`) | | `metric` | string | optional | Metric so sánh: `answer_rate_pct` (default) / `avg_duration_sec` / `transfer_rate_pct` | ### Response 200 ```json { "data": [ { "campaign_id": 5, "campaign_name": "Renewal Q3", "metric_value": "62.30" }, { "campaign_id": 8, "campaign_name": "Cross-sell Q3", "metric_value": "55.18" }, { "campaign_id": 12, "campaign_name": "Win-back churned", "metric_value": "48.92" } ] } ``` ## GET `/api/autocall/campaigns/{id}/reports/hangup` — Phân tích lý do cúp Giúp nhóm vận hành biết campaign fail vì lý do gì để optimize. ### Response 200 ```json { "data": { "total_calls": 4823, "by_hangup_cause": [ { "hangup_cause": "NORMAL_CLEARING", "count": 3120, "pct": "64.69" }, { "hangup_cause": "NO_ANSWER", "count": 1234, "pct": "25.59" }, { "hangup_cause": "USER_BUSY", "count": 287, "pct": "5.95" }, { "hangup_cause": "CALL_REJECTED", "count": 95, "pct": "1.97" }, { "hangup_cause": "RECOVERY_ON_TIMER", "count": 87, "pct": "1.80" } ] } } ``` Tham chiếu mã hangup: [Hangup cause codes](/tham-chieu/hangup-codes). ## GET `/api/autocall/campaigns/{id}/reports/heatmap` — Heatmap giờ × thứ Trực quan thời điểm hot nhất trong tuần để adjust schedule. ### Query parameters | Field | Type | Required | Mô tả | |---|---|---|---| | `metric` | string | optional | `dialed` (default) / `answered` / `answer_rate_pct` | ### Response 200 ```json { "data": [ { "day_of_week": 1, "hour": 9, "dialed": 156, "answered": 102, "answer_rate_pct": "65.38" }, { "day_of_week": 1, "hour": 10, "dialed": 178, "answered": 121, "answer_rate_pct": "67.98" }, { "day_of_week": 2, "hour": 14, "dialed": 201, "answered": 134, "answer_rate_pct": "66.67" } ] } ``` `day_of_week`: `0` = Chủ nhật, `1` = Thứ Hai, ... `6` = Thứ Bảy. Mảng phẳng 168 entries (7 ngày × 24 giờ). ## GET `/api/autocall/campaigns/{id}/reports/dtmf` — Phân tích DTMF chi tiết ### Response 200 ```json { "data": { "total_answered": 3120, "with_dtmf_input": 3005, "no_input": 115, "no_input_rate_pct": "3.69", "by_digit": [ { "digit": "1", "count": 1840, "pct": "61.23", "label": "Đồng ý chuyển agent" }, { "digit": "2", "count": 950, "pct": "31.61", "label": "Không quan tâm" }, { "digit": "0", "count": 215, "pct": "7.16", "label": "Nghe lại" } ], "by_script_path": [ { "path": "intro → press_1 → queue_sales", "count": 1840 }, { "path": "intro → press_2 → playback_polite → hangup", "count": 950 } ] } } ``` `label` được lấy từ `dtmf_actions[].label` của script. ## GET `/api/autocall/campaigns/{id}/reports/funnel` — Funnel chuyển đổi ### Response 200 ```json { "data": [ { "stage": "Imported lead", "count": 5000, "drop_rate_pct": null }, { "stage": "Dialed", "count": 4823, "drop_rate_pct": "3.54" }, { "stage": "Answered", "count": 3120, "drop_rate_pct": "35.31" }, { "stage": "Pressed valid DTMF", "count": 3005, "drop_rate_pct": "3.69" }, { "stage": "Transferred to agent","count": 1840, "drop_rate_pct": "38.77" }, { "stage": "Agent disposed", "count": 1640, "drop_rate_pct": "10.87" } ] } ``` `drop_rate_pct`: tỷ lệ giảm so với stage trước. ## POST `/api/autocall/campaigns/{id}/reports/export` — Xuất Excel ### Request body | Field | Type | Required | Mô tả | Giá trị | |---|---|---|---|---| | `report_type` | string | ✅ | Loại báo cáo | `kpis` / `trend` / `hangup` / `dtmf` / `funnel` / `full` | | `from` | date | optional | Cửa sổ from | YYYY-MM-DD | | `to` | date | optional | Cửa sổ to | YYYY-MM-DD | | `format` | string | optional | Format file | `xlsx` (default) / `csv` | ### Request Body (JSON) ```json { "report_type": "full", "from": "2026-06-01", "to": "2026-06-30", "format": "xlsx" } ``` ### Response 202 (Accepted, async) ```json { "data": { "job_id": "export_a7b3c5d2", "status": "queued", "report_type": "full", "estimated_seconds": 30 } } ``` ### Polling status ``` GET /api/autocall/campaigns/{id}/reports/export/{job_id} ``` ```json { "data": { "job_id": "export_a7b3c5d2", "status": "ready", "download_url": "https://app.zorio.vn/api/public/exports/...?signature=...", "expires_in_seconds": 3600, "file_size": 542600, "completed_at": "2026-06-30T08:01:23+00:00" } } ``` `status` enum: `queued` / `running` / `ready` / `failed` / `expired`. ### Download `download_url` là signed URL TTL **1 giờ** — follow redirect để tải file `.xlsx`. ## Best practice ### Cache report KPIs / Trend ít đổi (data aggregate mỗi phút). Cache 1-5 phút ở CRM để giảm load. ### Realtime vs Reports - **Realtime** (lead-level updates): subscribe webhook `autocall.lead.*`. - **Reports** (aggregate): poll endpoint này định kỳ (mỗi 1-5 phút). KHÔNG poll Reports mỗi giây — vừa lãng phí rate limit vừa không cần thiết. ### Export với volume lớn Campaign > 50k lead — export `full` có thể mất 1-2 phút. Tránh blocking UI: 1. Submit job qua POST → nhận `job_id`. 2. Poll status mỗi 5 giây. 3. `ready` → trigger download tự động hoặc hiển thị nút "Tải file". ## Tài liệu liên quan - [AutoCall — Tổng quan](/api/autocall/) - [AutoCall — Campaigns](/api/autocall/campaigns) - [AutoCall — Webhooks](/api/autocall/webhooks) - [Hangup cause codes](/tham-chieu/hangup-codes) --- # Scripts + DTMF actions # Scripts + DTMF actions Quản lý kịch bản TTS (template + voice + DTMF actions) cho AutoCall. Bao gồm CRUD scripts, enum DTMF action types, danh sách queue helper và preview TTS. ## Tạo script ```http POST /api/autocall/scripts ``` **Body**: ```json { "name": "Chăm sóc chuẩn", "description": "Template cho campaign chăm sóc khách hàng kích hoạt lại sau 30-60 ngày", "template_text": "Xin chào {{salutation_name}}, chúng tôi xin nhắc bạn về lịch hẹn ngày {{callback_date}}. Vui lòng bấm phím 1 để xác nhận tham gia, phím 2 để chuyển nhân viên hỗ trợ, phím 9 để bỏ qua.", "voice_id": "voice_id_premium_02", "tts_tier": "B", "max_repeat_per_attempt": 1, "status": "draft", "dtmf_actions": [ { "digit": "1", "label": "Xác nhận thanh toán", "action_type": "playback_then_hangup", "target_audio_url": "/media/thank_you.wav" }, { "digit": "2", "label": "Chuyển nhân viên", "action_type": "queue", "target_queue_name": "support@default" }, { "digit": "9", "label": "Bỏ qua", "action_type": "hangup" } ] } ``` **Fields**: | Field | Required | Type | Mô tả | |---|---|---|---| | `name` | Có | string, max:255 | Tên script | | `template_text` | Có | text | Template với placeholder `{{variable_code}}` | | `voice_id` | Có | string | ID giọng đọc — PHẢI lấy từ `GET /api/autocall/voices` ([Voices](/api/autocall/voices)). Hệ thống tự routing đến provider tương ứng. Truyền `voice_id` không tồn tại trong thư viện voice khả dụng -> **422**. | | `tts_tier` | Không | enum (`A`/`B`, default `B`) | **A** = premium, render realtime từ nhà cung cấp TTS cao cấp với key của khách hàng, tính theo char usage. **B** = local unlimited, dùng thư viện voice tiếng Việt khả dụng sẵn, không phí, throughput cao. Khuyến nghị default `B` cho khối lượng lớn. | | `max_repeat_per_attempt` | Không | integer (1-5, default 1) | Số lần phát lại audio nếu user không bấm DTMF | | `status` | Không | enum (`draft`/`active`/`archived`, default `draft`) | Chỉ script `active` mới được engine sử dụng cho campaign chạy thực tế | | `dtmf_actions[]` | Không | array max:12 | Cấu hình phím bấm, mỗi item gồm `digit`/`label`/`action_type`/`target_*` | ::: warning Field `voice_provider` KHÔNG có trong request body Khách hàng KHÔNG truyền. Hệ thống tự routing theo `voice_id` -> tier tương ứng (A premium / B local). Response server vẫn TRẢ field này — read-only, cho biết provider đã được sử dụng. ::: **Error 422 `voice_id` không tồn tại**: ```json { "message": "The given data was invalid.", "errors": { "voice_id": [ "voice_id 'voice_id_premium_01' không tồn tại trong thư viện voice khả dụng. Lấy danh sách voice hợp lệ qua GET /api/autocall/voices." ] } } ``` ### DTMF action types | `action_type` | Field bắt buộc kèm theo | Hành vi | |---|---|---| | `playback` | `target_audio_url` | Phát file rồi chờ DTMF tiếp | | `playback_then_hangup` | `target_audio_url` | Phát file rồi ngắt máy | | `switch_script` | `target_script_id` | Load script khác, replay từ đầu | | `queue` | `target_queue_name` (= `name` định danh queue) | Chuyển vào queue, agent live tiếp nhận | | `hangup` | — | Ngắt máy ngay | | `repeat_script` | — | Replay full audio script hiện tại | ::: tip Lấy enum động qua API Thay vì hardcode 6 action type ở client, KH có thể fetch enum đầy đủ từ `GET /api/autocall/dtmf-action-types` (xem mục [List action types](#list-action-types-cho-dtmf-enum-label)). Tương tự, danh sách `target_queue_name` lấy từ `GET /api/autocall/queues` (xem [List queue](#list-queue-cho-action-type-queue)). ::: ::: warning Validate ràng buộc tham chiếu (server-side) - `playback` / `playback_then_hangup` -> `target_audio_url` PHẢI khớp `file_path` của 1 media file thuộc tài khoản (lấy từ `GET /api/media-files`). Truyền URL ngoài -> 422. - `switch_script` -> `target_script_id` PHẢI là script tồn tại trong tài khoản (lấy từ `GET /api/autocall/scripts`). Truyền ID rác -> 422. - `queue` -> `target_queue_name` PHẢI khớp `name` của queue active (lấy từ `GET /api/autocall/queues`). Sai -> 422. ::: **Error 422 sample**: ```json { "message": "The given data was invalid.", "errors": { "dtmf_actions.0.target_audio_url": [ "target_audio_url '/data/wrong/path.wav' không tồn tại trong Media Files của khách hàng. Upload file qua POST /api/media-files trước, rồi dùng file_path trả về." ] } } ``` ## List action types cho DTMF (enum + label) ```http GET /api/autocall/dtmf-action-types ``` **Mục đích**: trả về enum 6 action type kèm label tiếng Việt + danh sách field bắt buộc kèm theo. KH integration dùng để build dropdown "Hành vi khi bấm phím" trong UI cấu hình DTMF mà không cần hardcode enum. **Response 200**: ```json { "data": [ { "code": "playback", "label": "Phát audio", "description": "Phát file âm thanh URL chỉ định, sau đó tiếp tục kịch bản.", "required_fields": ["target_audio_url"] }, { "code": "playback_then_hangup", "label": "Phát audio rồi cúp máy", "description": "Phát file âm thanh xong tự động cúp máy.", "required_fields": ["target_audio_url"] }, { "code": "switch_script", "label": "Chuyển sang kịch bản khác", "description": "Chuyển cuộc gọi sang kịch bản AutoCall khác (chuỗi IVR).", "required_fields": ["target_script_id"] }, { "code": "queue", "label": "Chuyển vào hàng đợi agent", "description": "Chuyển cuộc gọi vào hàng đợi để agent live tiếp nhận.", "required_fields": ["target_queue_name"] }, { "code": "repeat_script", "label": "Lặp lại kịch bản", "description": "Phát lại kịch bản hiện tại từ đầu (cho khách nghe lại).", "required_fields": [] }, { "code": "hangup", "label": "Cúp máy", "description": "Kết thúc cuộc gọi ngay lập tức.", "required_fields": [] } ] } ``` **Field response**: | Field | Type | Mô tả | |---|---|---| | `code` | string | Giá trị enum dùng cho `dtmf_actions[].action_type` khi `POST/PUT /scripts` | | `label` | string | Nhãn tiếng Việt hiển thị trong dropdown UI | | `description` | string | Mô tả ngắn để giải thích thêm cho user end | | `required_fields` | string[] | Danh sách field bắt buộc đi kèm khi pick action này (vd `playback` -> bắt buộc kèm `target_audio_url`) | ## List queue cho action_type=queue ```http GET /api/autocall/queues ``` **Mục đích**: trả về danh sách queue active để client dùng làm `target_queue_name` khi pick `action_type=queue`. Build dropdown "Chọn hàng đợi" trong UI. **Response 200**: ```json { "data": [ { "id": 1, "name": "support_q1", "queue_number": "2001", "strategy": "ring_all" }, { "id": 2, "name": "sales_outbound", "queue_number": "2002", "strategy": "longest_idle_agent" } ] } ``` **Field response**: | Field | Type | Mô tả | |---|---|---| | `id` | integer | ID queue trong DB (tham khảo) | | `name` | string | Tên kỹ thuật của queue — DÙNG làm `target_queue_name` khi POST/PUT script với DTMF `queue` action | | `queue_number` | string | Số extension queue (vd `2001`) — dùng hiển thị cho user end | | `strategy` | string | Chiến lược phân phối: `ring_all` / `longest_idle_agent` / `round_robin` / ... | ::: danger Lưu ý quan trọng Field `target_queue_name` trong `dtmf_actions[]` PHẢI khớp **`name`** của queue (không phải `queue_number` hay `id`). Nếu sai, dial sẽ fail vì hệ thống không nhận diện được queue. ::: ## Response 201 mẫu sau khi tạo script ```json { "data": { "name": "Chăm sóc chuẩn", "description": "Template cho campaign chăm sóc khách hàng kích hoạt lại sau 30-60 ngày", "template_text": "Xin chào {{tong_can_thanh_toan}}, chúng tôi xin nhắc bạn về lịch hẹn ngày {{ngay_den_han}}. Vui lòng bấm phím 1 để xác nhận tham gia, phím 2 để chuyển nhân viên hỗ trợ, phím 9 để bỏ qua.", "voice_provider": "tts_provider_01", "voice_id": "evln-vi-nature", "tts_tier": "A", "max_repeat_per_attempt": 1, "status": "draft", "created_by": 3, "tenant_id": 1, "updated_at": "2026-06-25T04:37:49.000000Z", "created_at": "2026-06-25T04:37:49.000000Z", "id": 4, "dtmf_actions": [ { "id": 1, "tenant_id": 1, "script_id": 4, "digit": "1", "label": "Xác nhận thanh toán", "action_type": "playback_then_hangup", "target_audio_url": "/media/thank_you.wav", "target_script_id": null, "target_queue_name": null, "sort_order": 0, "created_at": "2026-06-25T04:37:49.000000Z", "updated_at": "2026-06-25T04:37:49.000000Z" }, { "id": 2, "tenant_id": 1, "script_id": 4, "digit": "2", "label": "Chuyển nhân viên", "action_type": "queue", "target_audio_url": null, "target_script_id": null, "target_queue_name": "support@default", "sort_order": 1, "created_at": "2026-06-25T04:37:49.000000Z", "updated_at": "2026-06-25T04:37:49.000000Z" }, { "id": 3, "tenant_id": 1, "script_id": 4, "digit": "9", "label": "Bỏ qua", "action_type": "hangup", "target_audio_url": null, "target_script_id": null, "target_queue_name": null, "sort_order": 2, "created_at": "2026-06-25T04:37:49.000000Z", "updated_at": "2026-06-25T04:37:49.000000Z" } ] } } ``` ::: tip Field order + tenant_id Hệ thống serialize field theo thứ tự **insert/create** + auto-injected (`created_at`/`updated_at`/`id`) ở cuối. Client KHÔNG dựa thứ tự key — parse theo key name. `tenant_id` xuất hiện trong cả script level và mỗi `dtmf_actions[]` item — redundant nhưng đúng vì cả 2 entity đều có `tenant_id`. ::: ## List / show / update / delete | Endpoint | Mục đích | |---|---| | `GET /api/autocall/scripts?status=active` | List scripts (filter optional) | | `GET /api/autocall/scripts/{id}` | Chi tiết kèm `dtmfActions` | | `PUT /api/autocall/scripts/{id}` | Cập nhật. Nếu gửi `dtmf_actions` -> replace toàn bộ | | `DELETE /api/autocall/scripts/{id}` | Soft delete (nếu đang dùng cho campaign -> 422) | ### `GET /api/autocall/scripts` — list **Query params**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `status` | string | Không | Lọc theo trạng thái script | `draft` / `active` / `archived` | ```json { "data": [ { "id": 2, "tenant_id": 1, "name": "Nhắc lịch hẹn", "description": null, "template_text": "Công ty ABC trân trọng thông báo lịch hẹn dịch vụ {{ngay_den_han}} xác nhận tham gia trước 18 giờ {{ngay_den_han}}. Trường hợp đã xác nhận, vui lòng bỏ qua cuộc gọi này. Liên hệ tổng đài ấn phím 3. Xin cảm ơn", "voice_provider": "tts_provider_01", "voice_id": "voice_id_premium_02", "tts_tier": "B", "audio_fixed_paths": null, "max_repeat_per_attempt": 1, "status": "active", "created_by": 1, "created_at": "2026-06-22T03:34:01.000000Z", "updated_at": "2026-06-22T08:11:49.000000Z", "deleted_at": null } ] } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data[].id` | integer | ID script | Số nguyên dương | | `data[].tenant_id` | integer | ID khách hàng sở hữu script | — | | `data[].name` | string | Tên script | Tối đa 255 ký tự | | `data[].description` | string \| null | Mô tả | Tối đa 2000 ký tự | | `data[].template_text` | text | Template TTS với placeholder `{{variable_code}}` | — | | `data[].voice_provider` | string | Provider TTS (read-only, derive từ `voice_id`) | `tts_provider_01` / `local` / ... | | `data[].voice_id` | string | ID giọng đọc TTS | Lấy từ `GET /api/autocall/voices` | | `data[].tts_tier` | string | Tier TTS | `A` (premium realtime) / `B` (local pre-rendered) | | `data[].audio_fixed_paths` | object \| null | Cache các đoạn audio cố định (template không-biến) đã render. Không bắt buộc xử lý khi tích hợp | — | | `data[].max_repeat_per_attempt` | integer | Số lần phát lại audio nếu user không bấm DTMF | 1-5 | | `data[].status` | string | Trạng thái script | `draft` / `active` / `archived` | | `data[].created_by` | integer | User ID người tạo | — | | `data[].created_at` | datetime ISO 8601 | Thời điểm tạo (UTC, microseconds) | — | | `data[].updated_at` | datetime ISO 8601 | Thời điểm cập nhật gần nhất (UTC, microseconds) | — | | `data[].deleted_at` | datetime ISO 8601 \| null | Soft delete timestamp | null nếu chưa xoá | ::: tip Format timestamp API trả ISO 8601 với microseconds `YYYY-MM-DDTHH:mm:ss.uuuuuuZ` (vd `2026-06-22T03:34:01.000000Z`). Client parse theo ISO 8601 chuẩn (microseconds optional). ::: ### `GET /api/autocall/scripts/{id}` — chi tiết Object đầy đủ kèm `dtmf_actions[]`: ```json { "data": { "id": 2, "tenant_id": 1, "name": "Nhắc lịch hẹn", "description": null, "template_text": "Công ty ABC trân trọng thông báo lịch hẹn dịch vụ {{ngay_den_han}}, với số tiền là {{tong_can_thanh_toan}}. ... Xin cảm ơn", "voice_provider": "tts_provider_01", "voice_id": "voice_id_premium_02", "tts_tier": "B", "audio_fixed_paths": null, "max_repeat_per_attempt": 1, "status": "active", "created_by": 1, "created_at": "2026-06-22T03:34:01.000000Z", "updated_at": "2026-06-22T08:11:49.000000Z", "deleted_at": null, "dtmf_actions": [ { "id": 7, "script_id": 2, "digit": "1", "label": "Xác nhận thanh toán", "action_type": "playback_then_hangup", "target_audio_url": "/media/thank_you.wav", "target_script_id": null, "target_queue_name": null, "sort_order": 0, "created_at": "2026-06-22T03:34:01.000000Z", "updated_at": "2026-06-22T03:34:01.000000Z" }, { "id": 8, "script_id": 2, "digit": "3", "label": "Chuyển tổng đài", "action_type": "queue", "target_audio_url": null, "target_script_id": null, "target_queue_name": "support_queue", "sort_order": 1, "created_at": "2026-06-22T03:34:01.000000Z", "updated_at": "2026-06-22T03:34:01.000000Z" } ] } } ``` ### `PUT /api/autocall/scripts/{id}` — cập nhật **Request**: ```http PUT /api/autocall/scripts/2 Content-Type: application/json Authorization: Bearer ``` **Body** (partial update — chỉ gửi field cần đổi; nếu gửi `dtmf_actions` thì hệ thống xoá toàn bộ DTMF cũ + tạo lại theo array mới): ```json { "name": "Nhắc nợ đến hạn — v2", "description": "Cập nhật template cho Q3-2026", "voice_id": "voice_id_premium_01", "max_repeat_per_attempt": 2, "dtmf_actions": [ { "digit": "1", "label": "Xác nhận thanh toán", "action_type": "playback_then_hangup", "target_audio_url": "/media/thank_you.wav" }, { "digit": "3", "label": "Chuyển tổng đài", "action_type": "queue", "target_queue_name": "support@default" } ] } ``` **Response 200** (full script đã update + `dtmf_actions[]` mới): ```json { "data": { "id": 2, "tenant_id": 1, "name": "Nhắc nợ đến hạn — v2", "description": "Cập nhật template cho Q3-2026", "template_text": "Công ty ABC trân trọng thông báo lịch hẹn dịch vụ {{ngay_den_han}}, với số tiền là {{tong_can_thanh_toan}}. ... Xin cảm ơn", "voice_provider": "tts_provider_01", "voice_id": "voice_id_premium_01", "tts_tier": "B", "audio_fixed_paths": null, "max_repeat_per_attempt": 2, "status": "active", "created_by": 1, "created_at": "2026-06-22T03:34:01.000000Z", "updated_at": "2026-06-25T04:49:00.000000Z", "deleted_at": null, "dtmf_actions": [ { "id": 9, "tenant_id": 1, "script_id": 2, "digit": "1", "label": "Xác nhận thanh toán", "action_type": "playback_then_hangup", "target_audio_url": "/media/thank_you.wav", "target_script_id": null, "target_queue_name": null, "sort_order": 0, "created_at": "2026-06-25T04:49:00.000000Z", "updated_at": "2026-06-25T04:49:00.000000Z" }, { "id": 10, "tenant_id": 1, "script_id": 2, "digit": "3", "label": "Chuyển tổng đài", "action_type": "queue", "target_audio_url": null, "target_script_id": null, "target_queue_name": "support@default", "sort_order": 1, "created_at": "2026-06-25T04:49:00.000000Z", "updated_at": "2026-06-25T04:49:00.000000Z" } ] } } ``` ::: warning PUT replace DTMF Nếu body có gửi `dtmf_actions[]`, hệ thống xoá toàn bộ DTMF cũ của script + tạo lại theo array mới. Nếu KHÔNG gửi `dtmf_actions` -> giữ nguyên DTMF cũ. -> IDs của DTMF mới sẽ KHÁC với IDs cũ (cũ đã bị delete). ::: ### `DELETE /api/autocall/scripts/{id}` ```json { "data": { "deleted": true } } ``` **Response 422** (script đang dùng cho campaign active): ```json { "message": "Không thể xoá script đang được campaign sử dụng. Hãy archive campaign trước." } ``` ## Preview TTS — render demo ```http POST /api/autocall/scripts/{id}/preview ``` **Path params**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `id` | integer | Có | ID script cần preview | Số nguyên dương tồn tại trong tài khoản | **Body** (optional): ```json { "variables": { "salutation_name": "anh An", "amount": "500000", "callback_date": "2026-06-30" } } ``` **Body fields**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `variables` | object | Không | Map `{variable_code: value}` để substitute placeholder `{{xxx}}` trong template. Bỏ trống -> hệ thống dùng giá trị demo mặc định theo `data_type` của mỗi biến | Key phải khớp `variable_code` đã định nghĩa | **Response 200**: ```json { "data": { "audio_url": "/api/autocall/scripts/12/preview-audio/preview_abc123.wav", "duration_sec": 12.4, "chars_used": 187 } } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data.audio_url` | string | Đường dẫn nội bộ để stream file wav preview. GET với `Authorization` header trả binary `audio/wav` | Path relative cho `app.zorio.vn` | | `data.duration_sec` | number | Thời lượng audio đã render (giây) | > 0, làm tròn 1 chữ số thập phân | | `data.chars_used` | integer | Số ký tự đã sử dụng (tính phí với Tier A) | >= 0 | ::: tip Rate limit preview TTS preview giới hạn **10 req/phút/token** (vì charge TTS provider tier A). ::: --- # Voices + Media files # Voices + Media files Hai nhóm tài nguyên dùng chung trong AutoCall: - **Voices** — danh sách giọng đọc TTS khả dụng để dùng `voice_id` khi tạo script. - **Media files** — file âm thanh (.wav/.mp3/...) dùng làm `target_audio_url` cho DTMF action `playback` / `playback_then_hangup`. ## List voices dùng được trong AutoCall (KHUYẾN NGHỊ cho integration) ```http GET /api/autocall/voices ``` **Mục đích**: trả về danh sách **chỉ voice khả dụng**. Đây là voice **đảm bảo render preview/dial được** — KHÔNG bị lỗi `Voice không khả dụng` như khi pick voice tuỳ ý từ provider. **Response 200** (chỉ 5 field cần thiết cho client): ```json { "data": [ { "id": "voice_id_premium_02", "name": "vi-VN-female-01", "preview_url": "https://app.zorio.vn/api/autocall/voices/preview", "gender": "female", "language": "vi" } ] } ``` **Field response**: | Field | Type | Mô tả | |---|---|---| | `id` | string | Voice ID — dùng làm `voice_id` khi `POST /api/autocall/scripts` | | `name` | string | Tên hiển thị friendly | | `preview_url` | string \| null | URL mp3 sample để play thử (preview audio) | | `gender` | string \| null | `male` / `female` | | `language` | string \| null | `vi` / `en` / ... | ::: tip Fallback khi provider lỗi Nếu provider integration tạm thời không phản hồi (network/api key), endpoint vẫn trả về voice đó với chỉ field `id` + `name=id` để client KHÔNG bị lỗi — UI có thể fallback hiển thị ID raw. ::: ::: tip Tier A vs Tier B - **Tier A** = premium nhà cung cấp TTS cao cấp realtime (charge ký tự). - **Tier B** = local pre-rendered vi-VN-female-01 (không phí, throughput cao). Khuyến nghị default cho khối lượng lớn. `tts_tier` cấu hình ở level script (xem [Scripts](/api/autocall/scripts)). Hệ thống tự routing theo `voice_id` -> provider tương ứng. ::: ## Media Files (Mẫu ghi âm) Module Media Files chuẩn của Zorio — dùng để upload file âm thanh (.wav/.mp3/.m4a/...) và auto-convert sang WAV 16-bit PCM 8kHz mono. Trong AutoCall, dùng `file_path` của media file làm `target_audio_url` cho DTMF action `playback` / `playback_then_hangup`. **Category enum**: - `announcement` — thông báo cho DTMF action (vd "Cảm ơn anh/chị", "Đang chuyển nhân viên...") - `ivr` — prompt IVR (chào, menu...) - `moh` — music on hold - `library` — tổng quát ::: warning Permission Cần `manage_recordings` hoặc `admin` để upload/sửa/xoá; viewer có thể list + lấy audio. ::: ### List media files ```http GET /api/media-files?category=announcement ``` **Query**: | Param | Type | Mô tả | |---|---|---| | `category` | string, optional | Filter theo enum `ivr` / `moh` / `announcement` / `library`. Bỏ qua -> trả tất cả | **Response 200**: ```json { "data": [ { "id": 20, "tenant_id": 1, "name": "VNT v2", "file_path": "/media/ivr/studio-1776477792_1776477798_d939.wav", "file_size": 176718, "duration": 11, "format": "wav", "category": "ivr", "tts_text": null, "tts_voice": null, "tts_provider": null, "created_at": "2026-04-18 02:03:19" } ] } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data[].id` | integer | ID media file | Số nguyên dương | | `data[].tenant_id` | integer | ID khách hàng sở hữu | — | | `data[].name` | string | Tên hiển thị | Tối đa 255 ký tự | | `data[].file_path` | string | Định danh file (dùng làm `target_audio_url`) cho DTMF action `playback` | Định danh nội bộ | | `data[].file_size` | integer | Kích thước file (byte) | > 0 | | `data[].duration` | integer | Thời lượng audio (giây) | >= 0 | | `data[].format` | string | Định dạng audio (luôn là `wav` sau convert) | `wav` | | `data[].category` | string | Phân loại file | `ivr` / `moh` / `announcement` / `library` | | `data[].tts_text` | string \| null | Text gốc (chỉ != null nếu file synthesize qua TTS) | — | | `data[].tts_voice` | string \| null | Voice ID (chỉ với TTS file) | — | | `data[].tts_provider` | string \| null | Provider TTS đã dùng | `tts_provider_01` / `local` / null | | `data[].created_at` | datetime | Thời điểm upload (UTC, format `YYYY-MM-DD HH:mm:ss`) | — | ::: tip Field metadata TTS `tts_text`, `tts_voice`, `tts_provider` chỉ != null nếu file được synthesize qua TTS (không relevant cho file upload thủ công). Thông tin tham khảo, không bắt buộc xử lý. ::: ### Upload media file ```http POST /api/media-files Content-Type: multipart/form-data ``` **Body (multipart)**: | Field | Required | Type | Mô tả | |---|---|---|---| | `file` | Có | binary | File audio (.wav, .mp3, .m4a, .ogg, .aac, .flac). Max 50MB | | `name` | Có | string | Tên hiển thị (vd "Cảm ơn quý khách") | | `category` | Không | enum | `ivr` / `moh` / `announcement` / `library`. Default `library` | **Request mẫu (curl)**: ```bash curl -X POST https://app.zorio.vn/api/media-files \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" \ -F "file=@cam-on.mp3" \ -F "name=Cảm ơn quý khách" \ -F "category=announcement" ``` **Response 201**: ```json { "data": { "tenant_id": 1, "name": "Test", "file_path": "/media/library/test_1782374777.wav", "file_size": 43704, "duration": 3, "format": "wav", "category": "library", "id": 21 }, "message": "File đã upload và convert thành công." } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data.id` | integer | ID media file vừa tạo | Số nguyên dương | | `data.tenant_id` | integer | ID khách hàng sở hữu | — | | `data.name` | string | Tên hiển thị | — | | `data.file_path` | string | Định danh file sau convert (WAV 16-bit PCM 8kHz mono) | Định danh nội bộ | | `data.file_size` | integer | Kích thước file đã convert (byte) | > 0 | | `data.duration` | integer | Thời lượng audio (giây) | >= 0 | | `data.format` | string | Định dạng sau convert | Luôn `wav` | | `data.category` | string | Phân loại file | `ivr` / `moh` / `announcement` / `library` | | `message` | string | Thông điệp xác nhận thành công | — | **Error 422** (file sai định dạng / quá lớn): ```json { "message": "The file must be a file of type: wav, mp3, m4a, ogg, aac, flac.", "errors": { "file": ["The file must be a file of type: wav, mp3, m4a, ogg, aac, flac."] } } ``` **Error 500** (server thiếu engine chuyển đổi audio): ```json { "error": "engine chuyển đổi audio chưa được cài đặt trên server" } ``` ### Cập nhật metadata media file ```http PATCH /api/media-files/{id} ``` Chỉ đổi `name` / `category` — KHÔNG đụng file vật lý. **Path params**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `id` | integer | Có | ID media file cần cập nhật | Số nguyên dương | **Body**: ```json { "name": "Cảm ơn — phiên bản 2", "category": "announcement" } ``` **Body fields**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `name` | string | Không | Đổi tên hiển thị | Tối đa 255 ký tự | | `category` | string | Không | Đổi phân loại | `ivr` / `moh` / `announcement` / `library` | **Response 200**: ```json { "data": { "id": 21, "tenant_id": 1, "name": "Cảm ơn — phiên bản 2", "file_path": "/media/library/test_1782374777.wav", "file_size": 43704, "duration": 3, "format": "wav", "category": "announcement", "tts_text": null, "tts_voice": null, "tts_provider": null, "created_at": "2026-06-25 08:06:17" }, "message": "Đã cập nhật file âm thanh." } ``` **Field response**: shape giống `GET /api/media-files` item (xem table ở mục [List media files](#list-media-files)) + thêm `message` ngoài `data`. ### Thay thế file vật lý (giữ ID) ```http POST /api/media-files/{id}/replace Content-Type: multipart/form-data ``` Backup file cũ thành `.bak-` rồi ghi đè đúng `file_path` — IVR/DTMF action đang dùng `target_audio_url` không cần update vì path không đổi. **Path params**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `id` | integer | Có | ID media file cần thay | Số nguyên dương | **Body (multipart)**: | Field | Type | Required | Mô tả | Giá trị hợp lệ | |---|---|---|---|---| | `file` | binary | Có | File audio mới | `.wav`, `.mp3`, `.m4a`, `.ogg`, `.aac`, `.flac`, max 50MB | **Response 200**: ```json { "data": { "id": 21, "tenant_id": 1, "name": "Cảm ơn — phiên bản 2", "file_path": "/media/library/test_1782374777.wav", "file_size": 43704, "duration": 3, "format": "wav", "category": "announcement", "tts_text": null, "tts_voice": null, "tts_provider": null, "created_at": "2026-06-25 08:06:17" }, "backup": "test_1782374777.wav.bak-20260625-080956", "message": "Đã thay file thành công. File cũ đã backup." } ``` **Field response**: | Field | Type | Mô tả | Giá trị hợp lệ | |---|---|---|---| | `data` | object | Object media file (shape giống `GET /api/media-files` item) | — | | `backup` | string | Tên file backup của bản cũ. Path full = `dirname(file_path) + '/' + backup` | `.bak-` | | `message` | string | Thông điệp xác nhận | — | ::: tip Field `backup` ở top-level Không nằm trong `data` — chứa tên file backup. Đường dẫn full = `dirname(file_path) + '/' + backup`. ::: ### Xoá media file ```http DELETE /api/media-files/{id} ``` Xoá record DB + file vật lý. **Response 200**: ```json { "message": "Đã xóa." } ``` ::: warning Audit trước khi xoá Nếu file đang được DTMF action `target_audio_url` reference, dial sẽ fail với "file not found". Nên audit trước khi xoá. ::: ### Stream audio (play preview) ```http GET /api/media-files/{id}/audio ``` Trả về binary stream WAV với `Content-Type: audio/wav`. Dùng trong `