API Token (machine-to-machine)
API Token cấp cho hệ thống máy (CRM backend, dialer engine, integration service) gọi API Zorio mà không gắn với phiên user. Phù hợp cho giao tiếp server-to-server, tác vụ cron và các dịch vụ tích hợp (microservices).
Khác biệt với Bearer Token (User Session) hoặc Bearer Token (Phiên người dùng)
| Đặc điểm | Bearer (phiên user) (user) | API Token (machine) |
|---|---|---|
| Gắn với user nào | 1 user cụ thể | 1 ứng dụng / service |
| Khi user logout | Token mất hiệu lực | Vẫn sống |
| Khi user đổi mật khẩu | Còn sống | Còn sống |
| Audit log ghi nhận | Theo user.username | Theo tên ứng dụng |
| Phù hợp cho | App có người dùng (frontend, mobile) | Server-to-server / cron |
TIP
Cả 2 dạng đều dùng header Authorization: Bearer <token> giống nhau — phân biệt qua cách cấp + audit log, không phải qua format token.
Tạo API Token
Mỗi user tự tạo API Token cho tài khoản của mình — không cần admin cấp. Token gắn với user nào thì thừa hưởng quyền của user đó (có thể thu hẹp thêm bằng abilities).
Quy trình
- Đăng nhập vào tài khoản Zorio của bạn.
- Mở menu user (góc trên bên phải) → API Tokens. Trực tiếp:
https://<your-tenant>/profile/api-tokens. - Bấm + Tạo token mới:
- Tên — gợi nhớ mục đích (vd
hubspot-sync-prod,zapier-lead-import,cron-nightly-export). - Quyền (abilities) — tick các scope cần dùng (nguyên tắc least privilege: chỉ chọn quyền tối thiểu). Bỏ trống = quyền đầy đủ của user (
*). - Hết hạn — chọn ngày hết hạn hoặc bỏ trống (token không hết hạn). Với integration production, khuyến nghị set thời hạn ~1 năm và rotate định kỳ.
- Tên — gợi nhớ mục đích (vd
- Bấm Tạo token → hệ thống hiển thị token 1 lần duy nhất trong modal → bấm Copy và lưu vào secret vault của hệ thống.
- Đóng modal. Từ đây trở đi bạn chỉ xem được metadata (tên, quyền, thời điểm dùng cuối) — không xem lại token raw.
Endpoint tương đương (nếu bạn dùng API để tự tạo)
Sau khi login qua form, bạn có thể tự tạo token qua REST:
curl -X POST 'https://app.zorio.vn/api/auth/me/tokens' \
-H 'Authorization: Bearer <YOUR_SESSION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"name":"hubspot-sync","abilities":["leads:read","campaigns:read"]}'Response 201 chứa data.plain_token — sao chép ngay, không server nào trả lại lần thứ hai.
Rotate + Revoke
- Rotate: sinh token mới cùng tên/quyền/expires_at, revoke token cũ. Dùng khi nghi token bị lộ hoặc theo lịch xoay hàng tháng — bảng list có nút Rotate trên mỗi hàng.
- Revoke: huỷ token vĩnh viễn — bấm Revoke trên bảng list. Mọi integration đang dùng token này ngừng hoạt động ngay lập tức.
Giới hạn
- Tối đa 10 token active đồng thời mỗi user. Vượt → 422.
- Rate limit tạo token: 10 request / 60 giây mỗi user.
- Tenant admin có thể tắt tính năng self-service token cho toàn tenant (compliance) → user tạo token sẽ nhận 403.
- Mỗi lần tạo token thành công → user nhận email cảnh báo (kèm IP + timestamp + link revoke) để phát hiện tạo trái phép.
Dùng API Token
Giống Bearer (phiên user) — chỉ thêm header Authorization: Bearer <token> vào mọi request.
curl -X POST 'https://app.zorio.vn/api/pbx/calls/click-to-call' \
-H 'Authorization: Bearer <YOUR_MACHINE_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"from_extension":"1001","to_phone":"0987654321"}'Phạm vi quyền (Permission Scope)
API Token có quyền phụ thuộc vào Permission Scope đã được cấp — chỉ làm được thao tác trong danh sách permission đã chọn lúc tạo.
Ví dụ scope phổ biến:
| Scope | Cho phép |
|---|---|
pbx_api_access | Mọi endpoint /api/pbx/* |
manage_caller_ids | CRUD caller ID + groups |
telesales_api_access | Đọc/ghi campaign + lead Telesales |
autocall_api_access | Đọc/ghi campaign + lead AutoCall |
view_all_cdr | Xem CDR toàn tài khoản |
view_team_cdr | Chỉ xem CDR trong team |
Endpoint kiểm tra permission của token hiện tại:
GET /api/auth/me
Authorization: Bearer <token>Response 200 trả về mảng permissions.
Audit log
Mỗi request bằng API Token đều được ghi vào audit log của tenant với: user_id, route + method + status, IP, user_agent, request_id, duration_ms. Quản trị viên truy cập tại Admin Console → Audit Log.
Khi user tạo token mới, hệ thống gửi email cảnh báo tới địa chỉ email của user (kèm IP + timestamp + link revoke) — dùng để phát hiện thao tác trái phép nếu ai đó có được credential đăng nhập của bạn.
Rotate token
Khuyến nghị rotate định kỳ (90 ngày 1 lần với integration production):
- Mở
/profile/api-tokens→ bấm Rotate bên cạnh token. - Hệ thống sinh token mới cùng name/quyền/expires_at + revoke token cũ ngay lập tức → hiển thị token mới 1 lần → copy.
- Cập nhật token mới vào ứng dụng (deploy config mới hoặc rotate qua secret manager).
Không xoay 2 token cùng lúc
Nếu app dùng nhiều token (vd load balancer → N replicas), rotate từng cái một để có phương án quay lui (rollback) khi gặp lỗi.
Revoke
Token bị lộ hoặc không còn dùng — vào /profile/api-tokens → bấm Revoke. Mọi request với token sau khi revoke → HTTP 401.
Revoke ngay khi nghi ngờ lộ
- Code có token vô tình commit lên git public.
- Server có token bị hack.
- Nhân viên có quyền tạo token rời công ty.
Revoke ngay, không chờ điều tra. Tạo token mới + deploy.
Bổ sung token vào CI/CD
| Tool | Cách lưu token |
|---|---|
| GitHub Actions | Repository secrets → ZORIO_API_TOKEN |
| GitLab CI | CI/CD Variables (Masked + Protected) |
| Jenkins | Credentials → Secret Text |
| Vault | Path secret/zorio/api-token → fetch lúc deploy |
| K8s | Secret → mount vào env var pod |
Không ghi API Token vào log build. Tool CI thường có "masked output" — bật flag này.
Sample server-to-server (cron import lead)
// cron-job.js — chạy mỗi 5 phút
const axios = require('axios');
const api = axios.create({
baseURL: 'https://app.zorio.vn/api',
headers: {
Authorization: 'Bearer ' + process.env.OMNISERVE_TOKEN,
Accept: 'application/json',
'Content-Type': 'application/json',
},
timeout: 30_000,
});
async function syncLeads() {
const crmLeads = await fetchFromCRM();
for (const lead of crmLeads) {
try {
await api.post(`/telesales/campaigns/${lead.campaign_id}/leads`, {
phone: lead.phone,
custom_fields: lead.fields,
});
} catch (err) {
if (err.response?.status === 422) {
console.error('Lead invalid:', lead, err.response.data.errors);
} else {
throw err;
}
}
}
}
syncLeads().catch(console.error);