---
title: Smart Routings
excerpt: Shared concepts of the Smart Routings API, plus the inbound rules (call queues), groups, time spans, exceptions, import/export, inbound rule lookup and CFD token endpoints.
order: 9
---

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](/en/docs/smart-routings/introduction).

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](/en/docs/cx-api/smart-routings-ctis) | CTIs and their destinations, CTI lookups |
| [Contacts](/en/docs/cx-api/smart-routings-contacts) | Routing contacts and contact fields |
| [Geo Routing](/en/docs/cx-api/smart-routings-geo) | Geographic models, lists and destinations, geographic lookup |
| [Surveys](/en/docs/cx-api/smart-routings-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](/en/docs/cx-api/integration-keys) or [call history](/en/docs/cx-api/crm-calls)) 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`](/en/docs/cx-api/customers#get-apicustomersresolve) 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](#cfd-tokens), exchanged for a short `cfd`-scoped access token. Any OAuth token with the `client` scope also works on the lookups (see [Authentication](/en/docs/cx-api/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](/en/docs/cx-api/introduction#pagination)) and accept `filter[...]`, `sort` and `include` parameters as documented per endpoint. Text filters match partial values.
- Validation errors return `400` with a `hint` array:

```json
{
  "response": "bad request: please check body parameters.",
  "hint": ["The name field is required."]
}
```

- Business rule errors return a `message`:

```json
{ "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 `timezone` setting, see [`PUT /api/customers/settings`](/en/docs/cx-api/customers#put-apicustomerssettings)).

---

## 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

```json
{
  "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`**

```json
{ "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 |

```json
{ "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`**

```json
{ "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

```json
{
  "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`**

```json
{ "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

```json
{
  "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](#import-and-export) |

---

### 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`**

```json
{ "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.

```json
{
  "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 |

> [!WARNING]
> Without `ids`, **every** time span of the parent is deleted.

**Response `200`**

```json
{ "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

```json
{
  "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`**

```json
{ "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`**

```json
{ "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`**

```json
{ "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`**

```json
{ "response": "Import successful." }
```

**Response `422`** — invalid rows

```json
{
  "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:

1. an exception on that day;
2. a time span on that specific date (`date_full`) covering the time;
3. 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`**

```json
{
  "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`**

```json
{ "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

```json
{
  "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 `token` value 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`**

```json
{ "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`**

```json
{
  "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`**

```json
{ "message": "Host not authorised." }
```

**Response `404`** — unknown token, expired token, or its user no longer exists

```json
{ "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:

```php
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.
