English
English
Appearance
English
English
Appearance
API tokens are issued to machine clients (CRM backends, dialer engines, integration services) that call the Zorio API without being tied to a user session. They suit server-to-server traffic, cron jobs, and integration microservices.
| Property | Bearer (user session) | API Token (machine) |
|---|---|---|
| Tied to | One specific user | One app / service |
| When the user logs out | Token is invalidated | Still alive |
| When the user changes their password | Still alive | Still alive |
| Audit log entries | Use user.username | Use the application name |
| Best for | Apps with users (frontend, mobile) | Server-to-server / cron |
TIP
Both schemes use the same Authorization: Bearer <token> header — they're distinguished by how they're issued and how they show up in audit logs, not by token format.
Admins only
API tokens can only be created by an account admin through the Admin Console. There is no public API to self-issue tokens (this prevents a CRM from cloning its own tokens).
pbx_api_access, telesales_create_lead).Same as Bearer (user session) — just add Authorization: Bearer <token> to every 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"}'An API token has a narrower scope than a user — it can only perform actions listed in the permissions selected at creation time.
Common scopes:
| Scope | Allows |
|---|---|
pbx_api_access | Every /api/pbx/* endpoint |
manage_caller_ids | CRUD on caller IDs and groups |
telesales_api_access | Read/write Telesales campaigns and leads |
autocall_api_access | Read/write AutoCall campaigns and leads |
view_all_cdr | Read CDR for the entire account |
view_team_cdr | Read CDR only within a team |
To inspect the current token's permissions:
GET /api/auth/me
Authorization: Bearer <token>Response 200 returns a permissions[] array.
When a token is created with an IP whitelist:
{ "message": "Forbidden: IP not whitelisted." }.0.0.0.0/0 = accept every IP (the default).203.0.113.0/24).Load balancer note
If your app sits behind a load balancer / nginx proxy — Zorio reads the IP from the X-Forwarded-For header (the client, proxy1, proxy2 chain). Your LB must forward the real client IP correctly.
Every request made with an API token is logged with:
| Field | Description |
|---|---|
token_name | Token name (e.g. "CRM Salesforce Production") |
ip | Source IP |
endpoint | Method + path |
status | HTTP response status |
at | Timestamp |
Browse at Admin Console → Audit Log → API Token Activity.
For security, rotate tokens regularly (every 90 days is the recommended cadence):
Don't rotate two tokens at once
If your app uses several tokens (e.g. LB → N replicas), rotate them one at a time, not simultaneously, so you keep a rollback path if something breaks.
When a token is compromised or no longer needed — Admin Console → API Tokens → Revoke. Any request with the token after revocation → HTTP 401.
Revoke immediately on suspected leak
Revoke now, don't wait for an investigation. Mint a new token and redeploy.
| Tool | How to store the token |
|---|---|
| GitHub Actions | Repository secret → ZORIO_API_TOKEN |
| GitLab CI | CI/CD Variables (Masked + Protected) |
| Jenkins | Credentials → Secret Text |
| Vault | Path secret/zorio/api-token → fetch at deploy time |
| Kubernetes | Secret → mount as env var on the pod |
DO NOT echo tokens to build logs. Most CI tools have a "masked output" flag — enable it.
// cron-job.js — runs every 5 minutes
const axios = require('axios');
const api = axios.create({
baseURL: 'https://app.zorio.vn/api',
headers: {
Authorization: 'Bearer ' + process.env.ZORIO_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);