> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firetone.com.au/llms.txt
> Use this file to discover all available pages before exploring further.

# Trunks and rate cards

> Carriers, what they cost you, and what you charge for them.

export const Unverified = ({what, detail}) => <Warning>
    <strong>Not yet verified on a live call.</strong> {what} is built and covered
    by tests, but {detail} FireTone's engineering tracker records this as an open
    check rather than a passing one. Treat what follows as the design, and expect
    rough edges the first time you run it.
  </Warning>;

## Trunks

Carrier connections. A trunk is yours (shared with tenants) or a tenant's own.

A tenant's own trunk is never selectable by another tenant. That is enforced in
carrier **selection**, not merely hidden in the interface — otherwise it would
be unmetered calling on somebody else's carrier account, on their bill.

<Note>
  No trunk credential is ever returned by the API. They are stored encrypted and
  the panel has no way to display one.
</Note>

## A trunk's own page

Click a trunk's name. It answers, in the order the questions get asked:

* **Health** — whether it is registered, whether its configuration reached the
  switch, and whether anything is authorised to call in on it.
* **Channels now** — how many are up, against the total and outbound caps.
* **Who may call in** — the address list, editable there.
* **Usage** — what it carried, spend by organisation, and why calls ended.
* **Its calls**, an unanswered-only view, and recent warnings that mention it.
  **Carrier said** is the carrier's own answer to each call — `403`, `503`,
  `486` — which *Ended* (this side's cause) does not carry. A red one is a
  refusal somebody has to fix.

## Offers to organisations

Organisation admins choose their own outgoing routes, by destination, from
**offers** you make on **Outbound routing → Offers to organisations**. An offer
is a name organisations see ("Standard", "Premium") for one trunk at one
service level — *Standard* = Voxbeam · Gold — offered to every organisation or
just one. Organisations see the name and your description, never the carrier,
its prefixes or its dial strings; their choices become outbound rules for their
own organisation only, and they cannot name a trunk directly.

An offer an organisation is using cannot be withdrawn until those rules are
removed or changed.

## Connect, disconnect, verify

Three buttons at the top of a trunk's page:

* **Verify** runs every check that says whether this carrier can carry a call
  right now, and names the one that fails: in service, configuration on the
  switch, the carrier's host resolving, the gateway as the switch reports it
  (registered — or loaded to answer the carrier's challenge), a **SIP OPTIONS
  probe** from the server, inbound addresses, and the last 24 hours of calls.
  No answer to OPTIONS is a warning, not a failure: many carriers ignore it.
* **Disconnect** takes the carrier out of service at once: no new call uses
  it, its gateway comes off the switch and its addresses stop being accepted,
  so its inbound calls are refused too. Calls already up stay up.
* **Connect** puts it back. On a registering trunk that is already in service,
  **Register now** asks the switch to register immediately instead of at its
  next retry — what you want right after the carrier says "fixed, try again".

## A carrier that challenges calls

Some IP-authenticated carriers whitelist your address **and** challenge each
call for a username and password (Voxbeam's setup guide shows a gateway with
`register=false`). Put the credentials under *Carrier challenges outbound
calls* on the trunk: the switch then carries a non-registering gateway for it
and answers the challenge. The caller's number stays in the From header, as it
was.

## Who may call in

Inbound calls are accepted from listed addresses and refused from everything
else. The generated list is **default-deny**, so a trunk with no ACL hosts
accepts nothing at all — while looking enabled and healthy everywhere else.

<Warning>
  This is the commonest real fault on a trunk. If a carrier's calls never
  arrive, check its addresses first: ask the carrier for every signalling
  address, not just the one you registered to. Many send from a media gateway
  that is not the host in your configuration.
</Warning>

Carriers may send to port 5080 or 5060; both work, and a listed address is not
challenged on either.

A trunk that belongs to one organisation delivers **only that organisation's
numbers**; a shared trunk delivers numbers of any organisation you run. A call
to a number owned elsewhere is refused, and the log says *"the number belongs to
an organisation this trunk does not serve"* — not "unallocated", because the
number exists.

## Channel limits

**Channel limit** counts calls in **both** directions — it is the number your
carrier contracted.

**Outbound channel limit** caps the outgoing share of it. Without one, a
dialling burst can fill the carrier and leave nothing for customers ringing in,
with every screen looking healthy. Twenty against a total of thirty keeps ten
channels a burst cannot take. Leave it blank and the total is the only limit.

<Note>
  A call refused by either is congestion, and the log names which cap was
  reached — "the carrier is full" and "your own outbound reservation is full"
  need different fixes.
</Note>

## Codecs

A trunk has **two** codec lists, and the second one is optional.

* **Codecs for outgoing calls** is what FireTone offers the carrier when it
  dials out — and, unless you set the second list, what a call *arriving* on
  this trunk is held to as well.
* **Codecs for incoming calls** is what this carrier may offer on calls it
  sends you. Leave every codec unticked to accept the same list as above.

Set the second list when the carrier **sends** something different from what it
**accepts**. A carrier perfectly happy to take G.729 from you may well send you
G.711, and one list cannot say both.

<Warning>
  Until this second list existed, the outgoing list governed both directions —
  so a trunk set to `G729` to dial out cheaply was also refusing every call that
  carrier sent. There is no ringing when that happens: the call is refused while
  the codecs are still being agreed, so the caller hears a disconnection and the
  hangup cause is `INCOMPATIBLE_DESTINATION`. If you see that, compare the two
  lists — the daemon now logs both of them side by side, and the call's CDR
  records what the far end offered under **They offered**.
</Warning>

Both lists are **chosen from a list and put in order** — never typed.

* **Tick what the carrier speaks.** Each codec shows what it costs the switch to
  convert and who can speak it; no browser speaks G.729, iLBC or G.726.
* **Order the ticked ones.** Each call uses the *first* codec in this order that
  the other side also speaks, so the order is yours to choose: G.729 first saves
  bandwidth, G.711 first saves CPU. A newly ticked codec joins the end.
* **Nothing ticked** means the default, G.711 A-law then µ-law — and the editor
  says so rather than leaving a blank.

The editor warns only when there is something to warn about: a list no browser
can speak (every softphone call through the trunk is then transcoded — add G.711
behind it), a list with no G.711 at all, or — on a trunk pinned to a node — a
ticked codec whose module that node has not loaded, where calls would quietly
run in G.711 instead.

**Finding trunks by codec.** The trunks list has a *Speaks* filter, and the API
takes `GET /trunks?codec=G729`.

**Asking the carrier.** A list is what you *believe* the carrier accepts. **Test
codecs**, beside Verify on the trunk's page, asks it: one call attempt per
codec, offering that codec alone, hung up the moment the carrier rings and
before anyone answers, so nothing is billed. A codec the carrier will not take
comes back **refused (488)**, which is the same `INCOMPATIBLE_DESTINATION` a
customer's call would have died of. Give it your own mobile: it rings once per
codec for up to twenty seconds. A destination that answers without ringing
first is hung up within a second, which some carriers bill as a one-second
call. The API is `POST /trunks/{id}/codec-test`, one codec per call.

## Direction

A trunk carries calls **both ways** unless you say otherwise, and that is what
every trunk does until somebody changes it.

| Direction         | What it means                                                                                            |
| ----------------- | -------------------------------------------------------------------------------------------------------- |
| **Both**          | Calls go out and come in. The default.                                                                   |
| **Outgoing only** | A call that arrives on this trunk is **refused**, and the log says which trunk and that you declared it. |
| **Incoming only** | This trunk is never routed to, by any rule or fallback.                                                  |

A declaration is enforced, not decorative — that is the point of making it. On a
carrier account an unexpected inbound path is how toll fraud arrives, so a call
on a trunk you said would not receive one is refused rather than carried.

It also stops FireTone telling you something untrue. An outgoing-only carrier
has no use for ACL hosts, so **Accepts no inbound** is not shown for it and
`firetonectl doctor` stops reporting it — a false alarm the check printed on
every run until a trunk could say what it was.

<Note>
  It was a text box, and what was typed went to the switch as typed — which says
  nothing about a name it does not know. `ULAW,PCMA` offered PCMA alone; `ALAW`
  placed no call; `PCMA@30I` sent 20 ms packets, because a capital `I` is not a
  suffix. Lists are now stored in the one spelling the switch honours. A trunk
  saved before that keeps what was typed until it is next saved, and
  `firetonectl doctor` (which `verify.sh` runs) names any that need it.
</Note>

## Dial prefix

Digits this carrier wants in **front** of the number, added on the way out
after the `+` is removed. Blank for none.

<Warning>
  This is not the organisation's **outside-line prefix**, which travels the
  other way: that is what staff dial to reach a line, and it is stripped on the
  way in. One word, two settings, opposite directions.
</Warning>

## Number format

How numbers are sent to this carrier — the number dialled and the caller ID
alike:

| Format                         | `+61412345678` goes as                                                                                   |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
| International digits (default) | `61412345678`                                                                                            |
| International with +           | `+61412345678`                                                                                           |
| National                       | `0412345678` — for numbers in the carrier's country; any other number still goes in international digits |

National needs the carrier's country. The dial prefix, if any, goes in front
**after** formatting: prefix `1831` with national gives `18310412345678`.

## Caller ID

What the carrier is shown, chosen for each call in this order:

1. the extension's own caller ID number, when it is a real number (`1001` is not;
   `0412 345 678` is, read in the extension's country);
2. the organisation's **Outbound caller ID** — usually its main line;
3. the trunk's **Default caller ID** — usually the number the carrier account is
   registered as.

It is decided per carrier, so a call that fails over to a second trunk presents
that trunk's default rather than the first's. When none of the three is a real
number the call still goes out showing the extension number, and the log warns
that the carrier is likely to refuse it.

<Note>
  Campaigns keep their own caller ID and are not affected by this chain.
</Note>

## When the carrier refuses a call

The caller hears busy for busy, and "the call cannot be completed as dialled"
for anything else, instead of silence. The call's page shows **Far end** — the
carrier's SIP answer and cause — and the log line names the trunk, the SIP
status and the caller ID that was presented, which is usually the answer: a
`403` straight after a caller ID change is the carrier refusing the number.

`sudo deploy/staging/verify.sh` checks that every registering trunk on the box
is actually registered with its carrier, and `ftctl doctor` finds a registering
trunk that has lost its switch node.

## Service levels and outbound rules

<Note>
  The whole of outbound routing — the order a call is decided in, the three
  kinds of rule, what *Try a number* means by a carrier being "passed over" —
  is on its own page: [Outbound routing](/operator/outbound-routing). What
  follows is the part that belongs to a **carrier**: its service levels.
</Note>

Some carriers sell one account at several service levels, chosen per call by a
**tech prefix** in front of the number. Voxbeam, for example:

| Level       | Prefix    |
| ----------- | --------- |
| Platinum    | `0011102` |
| Gold        | `0011101` |
| Silver      | `0011103` |
| Call Center | `0011104` |

List them on the trunk's page under **Service levels**. Then choose between
them on **Outbound routing**, with **outbound rules**: a number pattern and the
legs a matching call takes, in order.

| Numbers starting with | Legs                                    |
| --------------------- | --------------------------------------- |
| `+91`                 | Voxbeam · Call Center                   |
| `+61`                 | Voxbeam · Platinum, then Voxbeam · Gold |
| `*` (any number)      | Voxbeam · Gold                          |

* The **most specific** pattern matching the number wins — `+9192` beats `+91`
  beats `*` — and only its legs are used. A `+91` rule does not quietly fall
  back to the `*` rule's legs.
* A rule for one organisation beats a shared rule for the same pattern.
* A leg's level **replaces** the trunk's dial prefix for that call. A leg with
  no level sends none.
* A number **no rule matches** goes out only through the organisation's own
  trunks, and is **refused** if it has none. It never goes to a shared carrier
  nobody chose. A rule for `*`, for all organisations or for one, is the
  default carrier.
* A call **fails over only to the legs its rule lists**. A rule whose carriers
  are all disabled or unusable refuses the call rather than trying others.
* Legs whose trunk is disabled are skipped, and `ftctl doctor` reports the rule
  (`outbound-rule-unusable`).
* A leg whose carrier has a [price list of its own](#a-trunks-own-price-list)
  that **does not cover the number** is passed over for the next leg. If no leg
  covers it, the call is refused and the log names the trunks.

**Try a number** on the same screen shows the legs a call to that number would
take and the exact dial strings, from the same code a call runs. The call's
page and the call records show which level carried each call.

A carrier that prices its levels differently can be priced that way: see
[a trunk's own price list](#a-trunks-own-price-list) below.

## Numbers a carrier sends without a +

A number arrives as `+919216217231`, `919216217231` or the national
`09216217231`. The first two are the same digits and match a DID without any
configuration. The third only means something if you know the country.

Switch on **Accept national-format numbers**, set the **Country**, and choose
how a parsed number is accepted:

| Validation       | Accepts                                                           |
| ---------------- | ----------------------------------------------------------------- |
| `libphonenumber` | numbers the country's rules call valid — the default              |
| `regex`          | whatever your pattern matches, tested against the resulting E.164 |
| `none`           | anything that parses                                              |

`regex` is the escape hatch for a carrier whose numbering the library judges
invalid. It still parses with the country's rules — a pattern can test a
number, it cannot convert one.

<Note>
  This is only ever consulted after the plain forms have failed to match, so
  switching it on cannot move a call that already works. It can only rescue one
  that was being refused.
</Note>

## Usage

**Trunks → Usage** compares carriers side by side: inbound and outbound counts,
answered rate, minutes and spend, with a trunk selected breaking spend down by
organisation. Every figure comes from the same place as the call list, so the
two cannot disagree about what a call is.

## A trunk's own price list

On a trunk's page, across the full width below *Who may call in* and *Service
levels*, **Destinations & pricing** holds what *that carrier* can reach and what
it charges: prefix, country, destination,
currency, price per minute, connect fee, billing (minimum / increment, so `60/60`
or `30/6`) and, optionally, a **service level**.

<Unverified what="A trunk's own price list" detail="no real call has yet been priced from one on a live installation, and the screens have not been exercised in a browser." />

It does **two** things, and the second is the one that surprises people:

1. **It prices that trunk's calls.** A row for the call's service level beats a
   row for *every level*; within each, the longest prefix wins. So Platinum and
   Gold on one carrier can cost what the carrier actually charges for them.
2. **It decides what the trunk can route.** A carrier's deck is also the list of
   where it can send a call. A number the list does not cover is **not sent to
   that trunk**: the next leg of the [outbound rule](#service-levels-and-outbound-rules)
   is tried, and if no leg's carrier lists the number the call is refused, with
   a log line naming the trunks and this screen.

The card says which of three states a trunk is in:

|                                    | Priced from                                                     | Routes                       |
| ---------------------------------- | --------------------------------------------------------------- | ---------------------------- |
| **Own price list**                 | this list                                                       | only numbers the list covers |
| **Shared cost card** (the default) | a cost card attached under [Billing](/operator/billing)         | everything                   |
| **No prices**                      | nothing — the call has no cost, so its margin is the whole sale | everything                   |

### The table

The prices are listed **in the card**, paged, searched and sorted on the server —
a deck is tens of thousands of rows and none of it is sent to the browser to be
sorted there.

* **Search** takes a number or a word. A number is matched **from its first
  digit**, with or without the `+`, so a dialled number pasted from a call record
  finds its destination (`+1876…` finds Jamaica; `44` does not find `1441`). A
  word is matched anywhere in the destination, the country or the service level.
* **Filters:** a service level — or *Prices for every level* — and a country.
* **Sorting:** prefix (the default, which keeps a prefix beside its own per-level
  prices), destination, service level, and **per minute** — the last only on a
  list in a single currency. Across several it would order numbers, not money.
* A price imported ahead of its date is listed with a **from *date*** badge; the
  one still in force sits on the row above it.

**Add a price** is a button. Everything done less often is in the **Menu**:
*Import…*, *Import history*, *Export*, *Quote a number…* and *Clear all…*. A
login that can see prices but not change them gets the table, the history,
Export and the quote, and none of the rest.

A trunk nobody has touched behaves exactly as it always did. **Clearing** a list
hands the trunk back to the shared card; it does not become a trunk that can
route nothing.

### Importing a carrier's deck

**Menu → Import…** takes the carrier's CSV more or less as it arrives. Column
names are matched loosely (`prefix` / `code` / `dial_code`, `price` / `rate` /
`cost`, `destination`, `currency`, `billing`, `service level` / `quality`,
`effective from`); prefixes may be written `+91`, `91` or `0091`; a title line
starting with `#` is skipped; and when there is no country column the country is
worked out from the prefix (`1876` is Jamaica, not the United States).

A vendor's rate sheet in its usual form is read as it stands:

| The sheet's column                 | Read as                                                                                                                                                          |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Prefix`                           | the prefix                                                                                                                                                       |
| `ISO`                              | the country code, **as the vendor wrote it** — a vendor's `AB` for Abkhazia is kept, not "corrected"                                                             |
| `Named Route Name`                 | the destination                                                                                                                                                  |
| `New Rate`                         | the price. `Existing Rate` is ignored — last month's price is never importable as this month's                                                                   |
| `Connection Unit`, `Bill Unit`     | the minimum and the increment: 30 and 6 is 30/6 billing                                                                                                          |
| `Currency`                         | the currency                                                                                                                                                     |
| `Effective Date`, `Effective Time` | when the price starts — `25-Sep-2026` and `20:00:00`, read as **UTC** unless you say the file is in another zone                                                 |
| `Status`                           | `Deleted`, `Removed`, `Closed`, `Blocked`, `Discontinued`… **withdraw** the destination; anything else (`Unchanged`, `Increased`, `Decreased`, `New`) is a price |
| `Expiry Date`, `Expiry Time`       | shown in the preview, and **not acted on** — see below                                                                                                           |

**A sheet usually arrives before its date.** Import it when it arrives. The new
prices are listed as *scheduled*, today's prices stay in force **and stay
listed** until that moment, and *What would a call cost?* goes on quoting
today's price until then. A trunk whose whole list is scheduled behaves as it
did before the import — it does not start refusing numbers five days early.

**A withdrawn destination** stops being priced on the vendor's date, and from
then the trunk is not sent those numbers: the next carrier in the outbound rule
takes them.

<Note>
  **Expiry dates are not obeyed.** A vendor's "expires 3 October" means "expect
  another sheet". If the list ended its prices then, a week with no new sheet
  would leave the trunk with no prices at all — and it would quietly stop being
  sent calls, or fall back to the shared card, because somebody was on holiday.
  Prices stay in force until a newer list replaces them.
</Note>

An **Excel workbook** (`.xlsx`, `.xls`) is refused with the way to fix it: *File →
Save As → CSV UTF-8*. Read as text a workbook is a zip file, and a complaint
about missing columns would send you to rename columns that are already right.

You choose what to assume **when the file does not say** — currency, minimum,
increment, connect fee, service level, the date the prices take effect — and one
of two modes:

| Mode                   | What happens to prices the file does not mention  |
| ---------------------- | ------------------------------------------------- |
| **Update**             | they stay                                         |
| **Replace everything** | they are removed: the file becomes the whole list |

**Preview first — the button says *Preview* until you have.** It shows how many
prices would be added, changed, left alone, removed and skipped, a few examples
of old against new, any row it could not use (by line, with the reason), and any
currency you have no [exchange rate](/operator/billing#exchange-rates) for.

<Warning>
  A price with a **seventh decimal place that is not zero is refused**, not
  rounded. Storing `0.0123456` where the carrier said `0.01234567` is an
  under-charge on every minute of every call, and rounding it is a decision
  about your money an importer should not make for you.
</Warning>

A file with **no usable rows** is refused in either mode. In *Replace* it would
otherwise remove every price and add none — and since a trunk with no list
quietly falls back to the shared card, the mistake would not even look like one.

A date in the future leaves today's price in force until then.

**What a file does not mention, it does not change.** A sheet dated Friday says
nothing about today, so today's prices stay until Friday — in either mode. And
in *Update*, correcting today's price for a destination leaves a price already
scheduled for Friday alone. (*Replace* makes the file the whole list: a scheduled
price it does not carry goes.)

**Clear all** ends everything that has not ended, scheduled prices included.

### Backup, and the same deck on another trunk

**Export** writes the whole list as the file the importer reads: every column,
service levels by *name*, nothing tied to the trunk it came from. Its first line
says which trunk, when, and how many prices.

* **Restore**: import a trunk's own export in *Replace* mode and the list comes
  back exactly — **including mid-week**, when it holds today's prices, the ones
  scheduled for Friday and a withdrawal due with them. All three are in the file
  (a withdrawal to come is written the way a vendor writes one: a `Deleted` row
  on that date), and restoring a backup over itself changes nothing.
* **Reuse**: import it into another trunk. A service level that trunk does not
  have is **counted and shown in the preview**, then left out or loaded for
  every level, as you choose — never quietly dropped or widened.

Import and export exist on the trunk and nowhere else. **Download the current
list first** is offered inside both the import dialog and the *Clear all*
confirmation.

### Import history

**Menu → Import history** lists every **import** and every **Clear all** on the
trunk, newest first: when, who, *Update* or *Replace*, the file's name and size,
and how many prices were added, changed, removed and left alone. Rows the
importer skipped open out under the entry.

* **The file is kept.** *Download* hands back exactly what was uploaded — the
  same bytes — which makes the history a backup of what each vendor actually
  sent, and the way to look again at a sheet from three months ago. Downloading
  needs the permission to *change* rates, where the list itself needs only the
  permission to see them: a vendor's sheet can hold more than was imported.
* **An entry means it happened.** It is written together with the prices, in one
  step. A *preview* is not in the history, nor is a file that was refused, and an
  import that failed part-way left neither prices nor an entry.
* The **newest twenty files** on a trunk are kept, and a file over 16 MB once
  compressed is not kept at all. Either way the entry stays, saying which.
* A price added or edited **by hand** is not here; that is in the audit log.

### What would a call cost?

**Menu → Quote a number…** It is priced, per service level, by the same
code that prices real calls: a price and the row that matched, **no price**, or
**not in this trunk's list — calls are not sent here**.

## Rate cards

A rate card is a set of per-destination prices with an effective date. Cards
come in two sides:

* **Cost** — what you pay the carrier.
* **Sell** — what the tenant pays you.

Both are matched by longest prefix, so a specific country beats a general one.

A cost card attached to several trunks is the **shared default**. A trunk with a
[price list of its own](#a-trunks-own-price-list) uses that instead.

## Prices are frozen at the call

A call is rated against the price that applied **when it ran**, and the rate id
is recorded on the record. A price is never overwritten: changing one ends the
old row and writes a new one, so the record still points at what the call was
charged. Changing a price today does not restate last month's invoices — which is the difference between a price change and an accounting
scandal.

## Money is exact

Amounts are exact decimals end to end, never floating point. A rate is a
fraction of a cent and binary floating point cannot represent it exactly; a
thousand calls is where that stops being theoretical.
