Smart Routings decide where an incoming call goes, based on inbound rules (call queues), opening hours, exceptions, geography, CTI keys or contacts. For a functional overview, see the Smart Routings documentation.
This page covers the concepts shared by every Smart Routings endpoint, then the inbound rule endpoints. The other resources have their own pages:
| Page | Resources |
|---|---|
| CTIs | CTIs and their destinations, CTI lookups |
| Contacts | Routing contacts and contact fields |
| Geo Routing | Geographic models, lists and destinations, geographic lookup |
| Surveys | Surveys and survey records |
Headers: Authorization: Bearer {workspace-token}
#Selecting the customer account
Smart Routings data belongs to a customer account. Every Smart Routings endpoint (and the other customer-scoped endpoints such as integration keys or call history) needs to know which customer it acts on. The customer is not deduced from the token: pass it in the request.
| Parameter | Description |
|---|---|
customer_account |
Customer account code (preferred) |
customer_id |
Customer ID, used when customer_account is absent |
account_id |
Alias of customer_id |
The parameter is usually sent in the query string (?customer_account=ACME01); it is also read from the body.
The request is rejected with 403 when:
- no customer matches the parameters (
No valid account specified.); - the authenticated user does not belong to that customer (
You do not have access to this account.); - the OAuth client is restricted to another customer (
This credential is not authorized for this account.).
Use GET /api/customers/resolve to check which customer a credential and a customer_account resolve to.
#Scopes and authentication
| Scope | Gives access to |
|---|---|
client |
Every Smart Routings endpoint |
cfd |
Lookup endpoints (inbound rules, geographic, CTI) and survey recording only |
Call flows usually authenticate with a CFD token, exchanged for a short cfd-scoped access token. Any OAuth token with the client scope also works on the lookups (see Authentication).
#Permissions
Permissions are checked against the authenticated user's role. The workspace owner and users with the superadmin role have every permission.
| Resource | Permissions |
|---|---|
| Inbound rules | call-queue.view, call-queue.create, call-queue.edit, call-queue.delete, call-queue.lookup |
| Groups | call-queue-group.view, call-queue-group.create, call-queue-group.edit, call-queue-group.delete |
| Time spans and exceptions | Same permissions as their parent: view to read, edit to create, change or delete |
| CFD tokens | cfd-token.view, cfd-token.create |
#Conventions
- Write endpoints expect a JSON body (
Content-Type: application/json), except the import endpoint (multipart). - Lists are paginated (see Pagination) and accept
filter[...],sortandincludeparameters as documented per endpoint. Text filters match partial values. - Validation errors return
400with ahintarray:
{
"response": "bad request: please check body parameters.",
"hint": ["The name field is required."]
}
- Business rule errors return a
message:
{ "message": "The provided time span conflicts with another span for this entity." }
- Destination types, used by inbound rules, time spans and exceptions:
extension,voicemail,call_queue,end_call,external_number,other. - Local time. Time spans and exceptions are evaluated in the customer's timezone (the
timezonesetting, seePUT /api/customers/settings).
#Inbound rules (call queues)
An inbound rule describes a 3CX queue or a DID number, its default destination, and optionally the group it inherits time spans and exceptions from.
#The inbound rule object
{
"id": 5,
"customer_id": 12,
"call_queue_group_id": 2,
"active": true,
"name": "Support",
"host_name": "acme.3cx.eu",
"type": "queue",
"code": "SUP",
"number": "800",
"did_number": null,
"default_destination_type": "voicemail",
"default_destination": "800",
"created_at": "2025-01-15T09:30:00.000000Z",
"updated_at": "2025-01-20T16:02:11.000000Z",
"group": { "id": 2, "name": "Business hours" }
}
| Field | Description |
|---|---|
type |
queue (identified by number) or did (identified by did_number) |
host_name |
3CX host name the queue lives on |
code |
Free code, max 50 characters |
call_queue_group_id |
Group the rule belongs to, or null |
#GET /api/smart-routings/call-queues/queues
Returns the inbound rules of the customer, most recently updated first.
Query parameters
| Parameter | Description |
|---|---|
customer_account |
Required. Customer account code |
filter[call_queue_group_id] |
Exact group ID |
filter[active] |
Exact value, 1 or 0 |
filter[type] |
queue or did |
filter[name], filter[code], filter[number], filter[did_number], filter[host_name] |
Partial match |
filter[search] |
Partial match on name, code, number or DID number |
include |
customer, exceptions, timeSpans |
sort |
id, created_at, updated_at, name, code, type, call_queue_group_id, resource (number or DID number). Prefix with - for descending. Default: -updated_at |
per_page |
Results per page (default: 20) |
Response 200 — paginated list of inbound rule objects
#GET /api/smart-routings/call-queues/queues/{id}
Returns an inbound rule with its group, exceptions and time_spans.
Response 200 — inbound rule object
#POST /api/smart-routings/call-queues/queues
Creates an inbound rule for the customer. When call_queue_group_id is set, the group's time spans and exceptions are copied to the new rule.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Name, max 50 characters |
type |
string | no | queue or did |
number |
string | no | Queue number, max 50 characters |
did_number |
string | no | DID number, max 255 characters |
host_name |
string | no | 3CX host name |
code |
string | no | Free code, max 50 characters |
active |
boolean | no | Whether the rule is active |
call_queue_group_id |
integer | no | ID of an existing group |
default_destination_type |
string | no | One of the destination types |
default_destination |
string | no | Default destination, max 255 characters |
Response 201 — the created inbound rule object
#PUT /api/smart-routings/call-queues/queues/{id}
Updates an inbound rule. Accepts the same fields as the creation, all optional.
Moving the rule to another group replaces its time spans and exceptions with copies of the new group's. Setting call_queue_group_id to null detaches it: the copies become the rule's own, editable rows.
Response 201 — the updated inbound rule object
#DELETE /api/smart-routings/call-queues/queues/{id}
Deletes an inbound rule with its time spans and exceptions.
Response 200
{ "response": "element deleted" }
#PATCH /api/smart-routings/call-queues/queues/bulk
Updates several inbound rules of the customer at once. Only the fields present in the body are changed. Requires call-queue.edit.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids |
integer[] | yes | IDs of the rules to update |
active |
boolean | no | Activate or deactivate |
default_destination_type |
string | no | One of the destination types |
default_destination |
string | no | Default destination |
{ "ids": [5, 6, 9], "default_destination_type": "voicemail", "default_destination": "800" }
Response 200 — array of the updated inbound rule objects
#DELETE /api/smart-routings/call-queues/queues/bulk
Deletes several inbound rules of the customer, along with their time spans and exceptions. Requires call-queue.delete. Rules of another customer are ignored.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids |
integer[] | yes | IDs of the rules to delete |
Response 200
{ "response": "elements deleted" }
#Groups
A group shares its time spans and exceptions with every inbound rule it contains. The rules receive locked copies: only their destination and destination_type can be changed on the rule itself.
#The group object
{
"id": 2,
"customer_id": 12,
"name": "Business hours",
"created_at": "2025-01-10T08:00:00.000000Z",
"updated_at": "2025-01-10T08:00:00.000000Z",
"queues_count": 4
}
queues_count is only present in lists.
#GET /api/smart-routings/call-queues/groups
Returns the groups of the customer, most recently updated first.
Query parameters
| Parameter | Description |
|---|---|
customer_account |
Required. Customer account code |
filter[name] |
Partial match |
include |
queues, customer, exceptions, timeSpans |
sort |
id, created_at, updated_at, name. Default: -updated_at |
per_page |
Results per page (default: 20) |
Response 200 — paginated list of group objects
#GET /api/smart-routings/call-queues/groups/{id}
Returns a group with its queues, exceptions and time_spans.
Response 200 — group object
#POST /api/smart-routings/call-queues/groups
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Name, max 50 characters |
Response 201 — the created group object
#PUT /api/smart-routings/call-queues/groups/{id}
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Name, max 50 characters |
Response 201 — the updated group object
#DELETE /api/smart-routings/call-queues/groups/{id}
Deletes a group. Its inbound rules are kept and detached from the group; the copies they had inherited become their own rows.
Response 200
{ "response": "element deleted" }
#Time spans
A time span routes calls to a destination during an opening period, either every week (day_of_week) or on a specific date (date_full). A time span belongs to an inbound rule or to a group: every time span endpoint that lists or creates spans identifies that parent with two query parameters.
| Parameter | Description |
|---|---|
time_spanable_type |
call_queue (inbound rule) or call_queue_group |
time_spanable_id |
ID of the inbound rule or group |
An inbound rule that belongs to a group cannot have its own time spans: create them on the group (422).
#The time span object
{
"id": 31,
"source_time_span_id": null,
"imported_at": null,
"time_spanable_id": 5,
"time_spanable_type": "call_queue",
"reference": "morning",
"destination": "800",
"destination_type": "call_queue",
"day_of_week": 1,
"date_full": null,
"start_time": "08:30:00",
"end_time": "12:30:00",
"created_at": "2025-01-15T09:35:00.000000Z",
"updated_at": "2025-01-15T09:35:00.000000Z"
}
| Field | Description |
|---|---|
day_of_week |
1 (Monday) to 7 (Sunday) |
date_full |
Specific date (YYYY-MM-DD). Takes priority over weekly spans on that day |
source_time_span_id |
Set on copies inherited from a group (locked rows) |
imported_at |
Set on spans created by an import |
#GET /api/smart-routings/call-queues/time-spans
Query parameters
| Parameter | Description |
|---|---|
customer_account |
Required. Customer account code |
time_spanable_type, time_spanable_id |
Required. Parent of the spans |
filter[reference], filter[destination], filter[day_of_week], filter[date_full], filter[start_time], filter[end_time] |
Filters |
sort |
id, created_at, updated_at. Default: -updated_at |
per_page |
Results per page (default: 20) |
Response 200 — paginated list of time span objects
#GET /api/smart-routings/call-queues/time-spans/{id}
Returns a time span with its parent (time_spanable).
Response 200 — time span object
#POST /api/smart-routings/call-queues/time-spans
Creates a time span on the parent given in the query string. Time spans created on a group are copied to every inbound rule of the group.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
start_time |
string | yes | Start time, HH:MM:SS |
end_time |
string | yes | End time, HH:MM:SS, after start_time |
day_of_week |
integer | yes* | 1 (Monday) to 7 (Sunday) |
date_full |
string | yes* | Specific date, YYYY-MM-DD |
destination_type |
string | no | One of the destination types |
destination |
string | no | Destination, max 50 characters |
reference |
string | no | Free reference, max 50 characters |
*Provide day_of_week or date_full. If both are sent, day_of_week is ignored.
Response 201 — the created time span object
Response 422 — the span overlaps another span of the same parent on the same day
#PUT /api/smart-routings/call-queues/time-spans/{id}
Updates a time span. Accepts the same fields, all optional. On a locked span (inherited from a group), only destination and destination_type can be sent (422 otherwise).
Response 201 — the updated time span object
#DELETE /api/smart-routings/call-queues/time-spans/{id}
Deletes a time span. Locked spans cannot be deleted (403): delete them on the group.
Response 200
{ "response": "element deleted" }
#POST /api/smart-routings/call-queues/time-spans/bulk
Creates the same opening period on several days at once: one span per value of day_of_week and of date_full. Requires the time_spanable_type and time_spanable_id query parameters.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
day_of_week |
integer[] | yes* | Days of the week, 1 to 7 |
date_full |
string[] | yes* | Dates, YYYY-MM-DD |
start_time |
string | yes | Start time, HH:MM:SS |
end_time |
string | yes | End time, HH:MM:SS |
destination_type |
string | no | One of the destination types |
destination |
string | no | Destination |
reference |
string | no | Free reference |
*Provide day_of_week, date_full, or both.
{
"day_of_week": [1, 2, 3, 4, 5],
"start_time": "08:30:00",
"end_time": "12:30:00",
"destination_type": "call_queue",
"destination": "800"
}
Response 201 — array of all the time spans of the parent
Response 422 — one of the spans conflicts with an existing span
#PATCH /api/smart-routings/call-queues/time-spans/bulk
Changes the destination of several time spans of the parent. Works on locked spans.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids |
integer[] | yes | IDs of the time spans |
destination_type |
string | no | One of the destination types |
destination |
string | no | Destination |
Response 200 — array of the updated time span objects
#DELETE /api/smart-routings/call-queues/time-spans/bulk
Deletes several time spans of the parent.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids |
integer[] | no | IDs of the time spans to delete |
Without
ids, every time span of the parent is deleted.
Response 200
{ "response": "elements deleted" }
Response 403 — one of the targeted spans is locked (inherited from a group)
#Exceptions
An exception overrides the time spans on a given day (public holiday, closure…). Exceptions take priority over time spans. Like time spans, they belong to an inbound rule or a group, identified with two query parameters on the list, create and bulk endpoints.
| Parameter | Description |
|---|---|
exceptionable_type |
call_queue (inbound rule) or call_queue_group |
exceptionable_id |
ID of the inbound rule or group |
An inbound rule that belongs to a group cannot have its own exceptions: create them on the group (422).
#The exception object
{
"id": 77,
"source_exception_id": null,
"exceptionable_id": 2,
"exceptionable_type": "call_queue_group",
"reference": "France:christmasDay",
"label": "Christmas",
"destination": "800",
"destination_type": "voicemail",
"day": "2026-12-25",
"created_at": "2026-01-05T10:00:00.000000Z",
"updated_at": "2026-01-05T10:00:00.000000Z"
}
source_exception_id is set on locked copies inherited from a group.
#GET /api/smart-routings/call-queues/exceptions
Query parameters
| Parameter | Description |
|---|---|
customer_account |
Required. Customer account code |
exceptionable_type, exceptionable_id |
Required. Parent of the exceptions |
filter[reference], filter[destination], filter[day] |
Filters |
sort |
id, created_at, updated_at, day. Default: day |
per_page |
Results per page (default: 20) |
Response 200 — paginated list of exception objects
#GET /api/smart-routings/call-queues/exceptions/{id}
Returns an exception with its parent (exceptionable).
Response 200 — exception object
#POST /api/smart-routings/call-queues/exceptions
Request body
| Field | Type | Required | Description |
|---|---|---|---|
day |
string | yes | Date, YYYY-MM-DD |
label |
string | no | Label, max 50 characters |
reference |
string | no | Reference, max 50 characters, unique per parent |
destination_type |
string | no | One of the destination types |
destination |
string | no | Destination, max 50 characters |
Response 201 — the created exception object
Response 400 — the reference already exists, or another exception exists on that day
#PUT /api/smart-routings/call-queues/exceptions/{id}
Updates an exception. Accepts the same fields, all optional. On a locked exception, only destination and destination_type can be sent (422 otherwise).
Response 201 — the updated exception object
#DELETE /api/smart-routings/call-queues/exceptions/{id}
Deletes an exception. Locked exceptions cannot be deleted (403).
Response 200
{ "response": "element deleted" }
#PATCH /api/smart-routings/call-queues/exceptions/bulk
Changes the destination of several exceptions of the parent. Works on locked exceptions.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids |
integer[] | yes | IDs of the exceptions |
destination_type |
string | no | One of the destination types |
destination |
string | no | Destination |
Response 200 — array of the updated exception objects
#DELETE /api/smart-routings/call-queues/exceptions/bulk
Deletes several exceptions of the parent.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids |
integer[] | yes | IDs of the exceptions to delete |
Response 200
{ "response": "elements deleted" }
Response 403 — one of the targeted exceptions is locked
#GET /api/smart-routings/call-queues/exceptions/holidays
Lists the countries available for public holiday import.
Response 200
{ "countries": ["Andorra", "Argentina", "Australia", "Austria", "Belgium", "...", "France", "..."] }
#POST /api/smart-routings/call-queues/exceptions/holidays
Creates one exception per public holiday of a country on the parent given by exceptionable_type and exceptionable_id. Without year, the holidays falling within the next 12 months (starting today, possibly spanning two calendar years) are created; with year, those of that calendar year. Each exception gets the holiday name as label and {country}:{holiday} as reference (e.g. France:christmasDay).
A holiday is skipped when the parent already has an exception on that day, so the request can be repeated without creating duplicates.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
country |
string | yes | Country name as returned by GET …/exceptions/holidays (e.g. France) |
year |
integer | no | Year (4 digits). Default: the next 12 months |
destination_type |
string | no | Destination type applied to every holiday |
destination |
string | no | Destination applied to every holiday |
Response 201 — array of the created exception objects (empty when every holiday already had an exception)
Response 422 — unknown country or invalid year
#Import and export
The inbound rules of a customer can be exported to and imported from a spreadsheet. The file has one row per time span; inbound rules without time spans have a single row.
| Row | Content |
|---|---|
| 1 | Section labels: Call queue, Time span |
| 2 | Column names |
| 3+ | Data |
| Column | Description |
|---|---|
name |
Rule name |
number |
Queue number, or DID number for did rules |
type |
queue or did |
host_name |
3CX host name, must be one of the customer's hosts |
group_name |
Group name; created if it does not exist |
active |
1 or 0 |
code |
Free code |
default_destination_type, default_destination |
Default destination |
day_of_week |
1 (Monday) to 7 (Sunday) |
full_date |
Specific date, YYYY-MM-DD |
start, end |
Times, HH:MM:SS |
destination_type, destination |
Destination of the time span |
reference |
Time span reference |
#GET /api/smart-routings/call-queues/export
Downloads the customer's inbound rules as inbound_rules.xlsx. Requires call-queue.view.
Response 200 — Excel file
#POST /api/smart-routings/call-queues/import
Imports inbound rules from an .xlsx or .csv file (CSV: ; delimiter, " enclosure). Requires call-queue.create. Send the request as multipart/form-data.
Rules are matched on type and number (or DID number): existing rules are updated, others are created. The whole file is validated first and imported in a single transaction: if one row is invalid, nothing is imported.
Form fields
| Field | Type | Required | Description |
|---|---|---|---|
document |
file | yes | .xlsx or .csv file |
replace_all |
boolean | no | Also delete the customer's rules missing from the file, and groups left empty |
Response 200
{ "response": "Import successful." }
Response 422 — invalid rows
{
"response": "The import failed validation.",
"failures": [
{
"row": 4,
"attribute": "host_name",
"errors": ["The selected host_name is invalid."],
"values": { "name": "Support", "number": "800", "type": "queue", "host_name": "unknown.3cx.eu" }
}
]
}
#Inbound rule lookup
#GET /api/smart-routings/call-queues/lookup
Returns the destination of an inbound rule at a given moment. This is the endpoint a call flow calls when a call comes in. Accepts the client or cfd scope and requires call-queue.lookup.
The destination is chosen in this order:
- an exception on that day;
- a time span on that specific date (
date_full) covering the time; - a weekly time span (
day_of_week) covering the time.
Only active rules are considered. Day and time are evaluated in the customer's timezone.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_account |
string | yes* | Customer account code |
customer_id |
integer | yes* | Customer ID |
queue_id |
integer | yes** | Inbound rule ID |
queue_number |
string | yes** | Queue number |
queue_name |
string | yes** | Rule name |
day |
string | no | Day to evaluate, YYYY-MM-DD. Default: today |
time |
string | no | Time to evaluate, HH:MM:SS. Default: now |
*Provide customer_account or customer_id.
**Provide one of queue_id, queue_number or queue_name (checked in that order).
Response 200
{
"reference": "morning",
"destination": "800",
"label": null,
"inherited": true,
"matched": "time_span,day_of_week"
}
| Field | Description |
|---|---|
reference |
Reference of the matched exception or time span |
destination |
Destination to route the call to |
label |
Exception label (null for time spans) |
inherited |
true when the match comes from the rule's group |
matched |
exception, time_span,date_full or time_span,day_of_week |
Response 404
{ "message": "No destination found matching given parameters." }
Other 404 messages: No customer found matching given parameters., No queue found matching given parameters.. When nothing matches, apply the rule's default destination in your call flow.
Response 400 — invalid parameters, or invalid day/time
#CFD tokens
A CFD token is a long-lived secret designed for call flows (3CX Call Flow Designer). It is attached to a user and exchanged for a short access token with the cfd scope, which only gives access to the lookup endpoints and to survey recording.
CFD tokens are workspace-level: they are not filtered by customer, but the endpoints still require customer_account.
#The CFD token object
{
"id": 3,
"user_id": 8,
"name": "Main call flow",
"token": "q8V3...80 characters...Xz",
"hosts": ["203.0.113.10"],
"expires_at": "2027-01-01T00:00:00.000000Z",
"created_at": "2026-01-05T10:00:00.000000Z",
"updated_at": "2026-01-05T10:00:00.000000Z",
"expired": false,
"user": { "id": 8, "name": "Call flow bot", "group": "admin", "email": "cfd@acme.com" }
}
The
tokenvalue is returned by every CFD token endpoint. Treat it as a password.
| Field | Description |
|---|---|
hosts |
IP addresses allowed to exchange the token. null or ["*"] allows any address |
expires_at |
Expiry date, or null for no expiry |
expired |
true once expires_at is in the past. Always false when expires_at is null |
The list endpoint does not return hosts.
#GET /api/smart-routings/cfd-tokens
Requires cfd-token.view.
Query parameters
| Parameter | Description |
|---|---|
customer_account |
Required. Customer account code |
filter[name] |
Partial match |
include |
user (always loaded) |
sort |
id, name, expires_at, created_at, updated_at. Default: -updated_at |
per_page |
Results per page (default: 20) |
Response 200 — paginated list of CFD token objects
#GET /api/smart-routings/cfd-tokens/{id}
Requires cfd-token.view. Non-admin users can only read their own tokens.
Response 200 — CFD token object
#POST /api/smart-routings/cfd-tokens
Creates a CFD token. The token value is generated (80 characters). Requires cfd-token.create.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
integer | yes | User the token authenticates as |
name |
string | no | Name, max 50 characters |
hosts |
string[] | no | Allowed IP addresses |
expires_at |
string | no | Expiry date, YYYY-MM-DD. The token expires at 00:00 UTC on that date. Omit it for a token that never expires |
Response 201 — the created CFD token object
#PUT /api/smart-routings/cfd-tokens/{id}
Updates a CFD token (user_id, name, hosts, expires_at). The token value cannot be changed. Allowed to admin users and to the token's owner.
Response 201 — the updated CFD token object
#DELETE /api/smart-routings/cfd-tokens/{id}
Allowed to admin users and to the token's owner.
Response 200
{ "response": "element deleted" }
#GET /api/smart-routings/cfd-auth/t/{token}
Exchanges a CFD token for an access token with the cfd scope. No Authorization header is needed.
Response 200
{
"message": "success",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
"expires_at": "2026-10-09T11:42:00+00:00",
"user": { "id": 8, "name": "Call flow bot", "email": "cfd@acme.com", ... }
}
Use the returned token as the Bearer token of the lookup calls. It is valid for 1 hour (expires_at): exchange the CFD token again on each call rather than storing the access token.
Response 403
{ "message": "Host not authorised." }
Response 404 — unknown token, expired token, or its user no longer exists
{ "message": "This token does not exists or has expired." }
The expiry is checked on every exchange: once expires_at is past, the CFD token can no longer be exchanged for an access token. A token without expires_at never expires. The check in the controller:
if (! $cfdToken || $cfdToken->expired) {
return response()->json([
'message' => 'This token does not exists or has expired.',
], 404);
}
Expiring or deleting a CFD token also revokes the access tokens it issued: the lookups made with them answer 401 straight away.