Telephony: numbering and routing
Applies to: SimpleTwo 0.9.x · Checked: 23.10.2026
The connection to the telephone network is served by the PSTN gateway — a separate role,
pstn, with SIP trunks to a carrier (sections 7 and 8): inbound numbers lead into meetings, to
people and to a voice menu where the caller types an internal number; outbound calls follow the
dial plan's rules. The clients offer dialling by themselves as soon as the server allows it to the
account — no separate build is needed (see «Switching it on» below). The mcu role is not the PSTN gateway: it brings
the meeting-room terminals you already own into meetings and works with neither trunks nor
telephone numbers — see Meeting rooms and SIP terminals. This page is about the
document you will manage outbound calls with: the dial plan. Internal numbers come from the same plan, and belong to
it alone: people have their range (2000–7999 by default), and meeting rooms, bots and guests may have
one of their own — without it rooms are numbered from the people's range and bots and guests get no
number. Two kinds of account never share a number. The directory keeps the plan and gives the numbers.
1. How it works
The plan is one versioned document per organisation. An administrator edits it in the console, the coordinator stores a new revision and pushes a snapshot to the communications plane.
The snapshot is pushed on change, not queried per call. That is deliberate: a control plane inside a telephone call means the coordinator's availability becomes the telephone's availability. While the coordinator is down, telephony runs on the last snapshot it received; the "push snapshot" button exists as a backstop.
The document holds:
| Part | What it defines |
|---|---|
| Sites | the one registry of places: an office (where an employee and a meeting room administratively belong — dialling country, timezone, its own trunks, main number) or a data centre (where servers stand) |
| Trunks | the carrier on the wire: where its calls come from, where ours go, digest and REGISTER; digit format, access prefix, numbers (DIDs and the registry it may present) |
| Rules | which number leaves through which trunk, presenting which caller ID |
| Emergency | a separate plan for emergency numbers |
| Destinations | which country codes are allowed at all |
| Quotas | limits on concurrency, rate and spend |
Since 20.10.2026 the site table is the one registry of places. A site has a kind: an
office is where people, meeting rooms and their devices are; a data centre is where servers
stand. A site's GEO is its country. Telephony reads offices only: a data centre has no main
number, no trunk or rule refers to one, and an account set to a data centre counts as having an
unknown site (the console refuses it). A server is placed in a data centre in its settings (Servers &
deploys); a host without that setting stands in the data centre made from its Secure Access GEO
entry (dc-ru for the entry RU). The MCU's SRV domains for meeting-room devices follow the GEO —
see «Meeting rooms».
2. Order is part of the plan
Rules are walked top to bottom and the first match wins. Trunks inside a rule are tried in the order written. The second is not a detail: it is how "primary and backup" is expressed. Spreading load over two trunks is written as two rules, not as a shuffle inside one.
3. The server normalises the number
The client sends exactly what the person dialled. E.164 is computed on the server, from the caller's site's country rather than the device's locale: the same seven digits from a phone with a German region and one with a Russian region are two different calls, and neither user did anything wrong.
The third state refuses here, unlike most of the product. The reason is simple: elsewhere the price of a wrong guess is a blank field, and here it is a bill and a fraud report.
4. Emergency numbers
Emergency is a separate plane, not a rule. It is matched first, on the raw digits, before normalisation and before every other gate; it is never failed over to a backup trunk and never counted against a quota.
It is not routed to emergency services. This is deliberate first-version behaviour: a refusal that is visible beats silently routing such a call "somewhere". Tell your users to dial 112 from a mobile phone.
5. Allowed destinations and quotas
Destinations are a list of allowed country codes, longest match wins (+7 is both Russia and
Kazakhstan, and an organisation may want one without the other).
Quotas are counted where the calls are, on the shared bus:
| Quota | What it bounds |
|---|---|
| concurrent per organisation / per trunk / per account | peak load; on a trunk incoming calls count too, and the trunk's own «Max» applies together with the plan's quota — the smaller of the two |
| calls per minute per account | a burst from one account |
| new destinations per hour | the usual signature of a guessed password |
| spend ceiling per window (a month — the default — or a day, UTC) — per organisation and per account | money — it blocks rather than alerting |
| longest call, minutes | a leg nobody noticed, billing all night |
| working hours | international calls outside the window |
The spend ceiling blocks on purpose: an alert at 03:00 is a message somebody reads at nine. The spend is the sum of the call records' cost estimates, each from its trunk's tariff; a call through a trunk without a tariff cannot be counted, so while a ceiling is set such a call is refused.
The concurrency, per-minute and new-destination counters live in the shared redis (the SFU pool's
bus) when messaging has one configured: a counter in one instance's memory is not a limit. Without
redis messaging runs as one instance and the counters are its memory (messaging's /healthz:
pstn_dialout.counters).
6. The tester and the stop switch
- Test a number — type one and see what the plan does with it: the normalised form, the rule that matched, the trunk it leaves through and the number it presents. Faster than placing a call and reading logs.
- Stop telephony — the stop switch: outbound calls cease, and the document records who pulled it and when. It is needed exactly when the bill is growing and the cause is not yet found.
- Push snapshot — force the communications plane to the current revision.
Every edit to the plan is audited, like any administrative action.
7. Trunks: the carrier on the wire
Under each trunk in the table there is a second row, the carrier on the wire:
| Field | What to enter |
|---|---|
| Carrier sources | IPs or CIDRs its INVITEs come from. A call is accepted only from these; the gateway drops everything else without a reply. 0.0.0.0/0 and anything wider than a /8 do not save |
| Challenge inbound | for a carrier whose addresses cannot be listed: the gateway asks its INVITE for the trunk's digest |
| Outbound address, transport, port | where our calls go — a name or an IP, no sip:, no port; TLS wherever the carrier supports it (default port 5060, 5061 for TLS) |
| SIP domain | the carrier's domain when it is not the address: the host of an outbound From and of the registration |
| Digest username and password | as the carrier issued them. The password is write-only: stored sealed (under the same master key as DKIM and certificates) and never shown again; a blank field keeps it, «clear password» removes it |
| REGISTER with the carrier and expiry | for a carrier that requires registration: the gateway keeps it itself, refreshes at 4/5 of the expiry and withdraws it when it stops |
| Tariff | one line per prefix: prefix price-per-minute [step-s [connect]], prices in minor units of the quota currency (7 300 60, 7495 100 1); the longest prefix wins. The call records estimate a call's cost by it, and the spend ceiling is kept with it: while a ceiling is set, a trunk with no tariff carries no call |
A trunk's numbers are also its DIDs: when two trunks arrive from one address, a call belongs to the one whose number was dialled. If neither has it, the call is refused.
The password is not part of the plan. The coordinator keeps it apart and sends it to messaging only, on a push of its own beside the plan snapshot; the gateway asks messaging, per call, which trunk this is and with which digest to dial. While encryption is locked (the master key not loaded) a new password does not save and the trunks are not pushed — messaging works from the copy it has.
8. The PSTN gateway: deploying it, and what works today
It is a separate role, pstn, not mcu. Meeting-room terminals and carrier trunks are served
by different hosts: they have different peers (your devices versus a carrier's SBC), a different
threat model (toll fraud is only possible on a trunk) and the same default ports — which is why
they never share a host.
Before you deploy
- messaging — the gateway takes its trunks' outbound calls from it, learns from it whose trunk an inbound call is on, and asks it for a pass into every room. There is no SFU key on the gateway host, and there never will be. The gateway reaches messaging at its public address (HTTPS): it needs no redis bus and no private network, so it can stand beside a carrier in another country.
- An SFU — the gateway joins a room as a participant, at the SFU's public name.
- The host: 4 vCPU / 4 GB carries roughly 80 concurrent calls (every call transcodes, ~0.05 of a core each; ~320 kbit/s per call on the node). A routable IPv4 on the interface, or 1:1 NAT with 5060 and the RTP range forwarded; symmetric NAT does not work, and the SIP side is IPv4 only.
Without a link to messaging the gateway neither takes nor places calls: messaging decides every one.
Deploying
Servers → Add a host → role «PSTN-шлюз (транки оператора)» (the PSTN gateway):
| Field | What to enter |
|---|---|
| Private address | optional: the gateway needs no private network |
| Public DNS name | optional; with one the coordinator orders a certificate and SIP-TLS opens on 5061 |
| Trunk-facing address | the IPv4 on this host the carriers' SBCs send to (empty = every interface) |
| Advertise IP | the public IPv4 for SDP, Via and Contact behind 1:1 NAT (empty = discovered) |
| RTP range | 10000-20000 by default; two ports per call |
Run the command on the host. The agent installs simpletwo-pstn and the libraries it is built
against (libopus, libopusfile, libsoxr), opens 5060/udp+tcp and the RTP range, and 5061/tcp only
once the certificate is on disk. The Servers row shows whether 5060 is listening.
If the coordinator says «this coordinator has no simpletwo-pstn binary», this release does not set
PSTN_VERSION yet — the gateway's first release is published under a tag of its own. The host
waits and converges by itself once the coordinator is updated.
Several gateways
There may be as many gateways as you need — one beside each country's carrier, say. Each trunk has exactly one gateway: on the trunk's card (Telephony → Numbering & routing, the PSTN gateway field) choose the one whose address the carrier sends to. While there is one gateway there is nothing to choose: every trunk goes through it. Once there are more, a trunk with no gateway chosen neither takes nor places calls, and the console warns about it under the tables.
Each gateway sees its own trunks only: it accepts inbound calls from their carriers only, keeps REGISTER with them only, and is handed their outbound calls only. When a trunk's gateway is not connected to messaging or is at its limit, a call on that trunk is refused at once with «gateway unavailable», and the rule's next trunk is tried where the rule allows it.
There is no redundancy: two gateways do not share a trunk, and a trunk's inbound calls are not spread across nodes.
What works today (0.9.x)
-
The role deploys, updates and shows in Servers; ports and the 5061 certificate as above.
-
The gateway holds no SFU key: messaging signs every pass into a room, at a door for which the gateway has a key of its own (not the MCU's).
-
Trunks: the gateway asks messaging which trunk an INVITE came from (by address, then by number), where an outbound leg goes and with which digest, and keeps the REGISTER itself with the carriers that require one.
-
Sites, trunks and groups' rules are directory objects. The plan's offices and data centres are directory sites; a trunk is an attribute of its PSTN gateway's service account, and its password a secret of that account which only messaging reads. A group or a department may have rules of its own — the Rules for groups and departments card beside the plan: a person several policies reach gets the one with the smallest order whole (its rules replace the organisation's, they are not added to them); everybody else dials by the organisation's. The test box applies the account's policy. The inbound numbers are the directory's too, and messaging reads them itself; a colleague's internal number dialled from the main number's menu it finds by the accounts' addresses in the directory.
-
The dial plan is kept by the directory: the console saves it there, and messaging reads it from there itself every 10 seconds, keeps it and survives a restart. While the directory holds no plan (before the first save, or before
upgrade1carries it), the plan still reaches messaging from the console (the «push snapshot» button, plus a reconcile every 15 minutes). The stop switch already works: with outbound telephony halted, messaging refuses the gateway before it dials a carrier. -
Outbound (the server side): messaging takes a request to call a number from a conversation or a meeting, checks the account's right (the
calls:pstnpermission — in no built-in role, granted explicitly) and the room's (whoever hosts the conversation or the meeting; never in an end-to-end conversation), runs the dial plan, the quotas and the spend ceiling, and only then hands the leg to the gateway that carries the chosen trunk. The call's progress — dialling, ringing, talking and the outcome — shows in the room and in the conversation. -
Call records (CDRs): every leg and every refusal is written to messaging's database — who, from which conversation, which number and which digits went to the trunk, which trunk, the attempt, answer and end times, the duration, the outcome and a cost estimate from the trunk's tariff (empty when there is none). Kept 395 days (
MSG_PSTN_CDR_RETENTION_DAYS). In the console: Telephony → Telephone call records (needsaudit:read), with the answer rate, the average call and the spend. -
The exact outcome: the gateway reports every leg to messaging itself — ringing, the carrier playing a message, answered, confirming, the end — with the SIP code and the Q.850 cause when the carrier sends one. So the outcomes are told apart: busy, declined, no answer, number unreachable, rejected by the carrier, line unavailable, not confirmed. The answer and end times in the records are the gateway's, not a poll's. If the gateway says nothing for 5 seconds, messaging polls the SFU as before (
/healthz→pstn_call_state). -
The carrier's early media (a 183 with SDP: «the subscriber is unavailable», network ringback) is heard in the room before the answer; in a personal call with no conversation the gateway plays its own ringback while the carrier is silent.
-
«Press 1» per dial-plan rule, the «Press 1» column: never (the default), group calls (not a one-to-one conversation) or always. Whoever answers hears the English prompt «To join, press 1» and enters the call only after the 1; 30 seconds without it hangs up as «not confirmed». Until they press, the room does not hear them and they do not hear the room.
-
Clients (iOS, macOS, Android, Windows, web): dialling out — the organisation's line in the choice of how to call, «Add a phone» in a call, «Call back» on a missed call — appears when the coordinator tells the account it may call out: its office's country allows outbound dialling, telephony is not stopped, and the account holds
calls:pstn. Everything that only shows a telephone in a call — a call from a number, the telephone's row and state in the participant list, the keypad, a recorded name in the lobby, the missed-call message, a meeting's dial-in numbers — shows whenever a telephone is there. Numbers are shown masked (+7916•••••67).
What does not exist yet
- Trunk calibration: mapping a carrier's codes onto outcomes is the same for every trunk (the per-trunk table is not built); the «press 1» prompt is English only.
- Trunks: SRTP per trunk is not configurable yet; REGISTER over TCP/TLS through NAT (RFC 5626) is not supported.
Switching it on
Telephony reaches users once every step is done — and the order matters: the clients offer dialling as soon as the server allows it.
- A carrier contract: a SIP trunk, its SBC addresses, the digest login and password (if it asks for them), the numbers (DIDs) and the tariffs.
- A host for the
pstnrole — a separate VM (not an MCU node: both listen on 5060/5061), with a public IPv4 or 1:1 NAT. Deploy the role as above and wait for «Servers» to show 5060 listening. - Countries: the country's row (for Russia —
RU, code7, trunk prefix8, international810, 00, length10), the «Dialling» tick and the «Written answer» — who allowed outbound calls and when (section 4: emergency numbers are not carried, a call to one is refused). - Sites: offices with their country; people and meeting rooms in their office.
- Trunks: the carrier's trunk (section 7) with its numbers and tariff.
- Rules: which numbers leave through which trunk and with which presented number.
- Destinations and quotas (section 5): the allowed country codes, the limits and the spend ceiling.
- The number tester (section 6): a few numbers — local, mobile, international, 112 — must give the expected trunk or refusal.
- Inbound numbers (section 8): the main number (menu) and the meetings' numbers; the prompt language.
- The
calls:pstnpermission — to those who may call out: no built-in role has it.
8. Inbound numbers
Telephony → Inbound numbers (PSTN gateway). Every number (DID) the carrier gave the organisation is assigned what it reaches:
| Reaches | What the caller hears |
|---|---|
a meeting — the meeting's room (meet-…) or its join link | joins that meeting while it is on, with no PIN; outside its time, a refusal |
| PIN prompt — the conference number | «enter the PIN» and «#»; three attempts, then a refusal |
| a person — an employee's login | ringback while the person's apps ring, exactly like a colleague's call; after «Ring a person, seconds» (30 by default) the call ends |
The document is kept by the coordinator and pushed to messaging (on save, by the button, and every 15 minutes while a gateway is deployed); the coordinator is never asked per call.
A meeting's PIN is nine digits, its own for every meeting. Nobody chooses it, and it opens the meeting only while the meeting is on: the PIN from last year's invitation leads nowhere. The numbers and the PIN of a meeting are in the «Dial-in for a meeting» block of the same card: paste the room or the link. Wrong PINs are counted: three per call, and a number that keeps typing them (or a whole trunk) is not accepted for 15 minutes.
A telephone caller is anonymous. The number they call from names their row in the meeting
(masked by default: +7916•••••67) and decides nothing — it lets nobody in and is never matched
to a colleague. By default the caller waits at the meeting's door — in the same waiting list as a
guest — until a host lets them in («Telephone callers in a meeting: always wait»). «Follow the
meeting's lobby setting» lets them straight into a meeting with no lobby, but only once a host is
inside: a telephone never starts a meeting alone. While waiting the caller hears ringback (a
meeting's own number) or a short double tone every 6 seconds (after a PIN); nobody letting them in
within «Wait at the door, seconds» (600 by default) ends the call.
A person in Do Not Disturb is never rung by a telephone — the caller is refused.
What the carrier hears on a refusal — a SIP code per reason (it used to be 404 for all):
| Reason | SIP |
|---|---|
| the number is assigned to nobody | 404 Not Found |
| the meeting is not on now; the person cannot take calls or is in Do Not Disturb | 480 Temporarily Unavailable |
| the person is already being rung by another telephone; the person pressed «busy» | 486 Busy Here |
| the person declined | 603 Decline |
| nobody answered within the ring time | 480 Temporarily Unavailable |
| the trunk's last channel is taken (quota) | 503 + Retry-After: 30 |
| telephony is halted by the stop switch | 503 + Retry-After: 300 |
| the inbound-numbers document has not arrived yet | 503 + Retry-After: 60 |
| no SFU / a storage error | 503 + Retry-After: 10 |
| not permitted; PIN locked or wrong | 403 Forbidden |
A call to a person (or to the main number's operator group) rings all their devices, and one answers. The first to answer talks; everybody else — and the answerer's other devices — stop ringing. «Decline» declines it for whoever pressed it; once everybody rung has declined, the caller hears 603 (486 for «busy») and each of them is left a missed-call message.
Incoming calls are in the same records as outgoing ones («Telephone call records», marked «in»): the caller's number (as the carrier delivered it — recorded, deciding nothing), the number called, the trunk the call came in on, what it reached, when it was let in or answered, and how long it lasted. An incoming call takes a channel on its trunk: when the trunk's «Max» is reached the next call is refused as busy (SIP 503), and an outgoing call on that trunk is refused by the quota.
The main number and extensions
Target «main number»: the caller hears a greeting and types a colleague's internal number (the extension on their user card, 2000–7999), a meeting room's internal number or a meeting ID (the meeting's PIN), then «#»; a 4-second pause after a digit counts as «#». «0», or silence, rings the number's operators: the logins in the third column, comma separated — one person or up to ten at once; the first to answer takes the call and the others stop ringing. A wrong number is asked again, three attempts, with the same brute-force limits as a PIN. The coordinator sends the extensions to messaging with the numbers on every push (save, the button, the 15-minute reconcile).
A meeting room's number leads into the meeting held in it now by its schedule — through that meeting's door like any dial-in: the lobby, the recorded name, a host lets them in. When the room has no meeting now, the caller hears so and may type another number — that is not counted as a wrong attempt.
The organiser PIN and keypad commands
Besides its PIN every meeting has an organiser PIN — the «Dial-in for a meeting» lookup shows
both; give it only to whoever runs the meeting. Typed instead of the PIN on entry, it lets the
organiser straight in, without the lobby, and the meeting may start with them. In the meeting:
*8, the organiser PIN, # makes the telephone a host (and lets it in if it was waiting at
the door); three wrong ones and host commands are no longer offered on that call. The caller's
number never makes anybody the organiser.
| Command | Who | What it does |
|---|---|---|
*6 | anyone | mute / unmute yourself (unmuting is refused while the host has turned microphones off) |
*1 | anyone | how many people are in the meeting |
*0 | anyone | help |
*7 | host | lock / unlock the lobby: while locked, everybody but the hosts waits at the door — from a telephone and from the app alike |
*9 | host | let in everyone who is waiting |
*5 | host | mute everyone but yourself |
Meeting rooms' devices on the MCU have the same keypad (see "The keypad and spoken prompts" on the meeting rooms page).
Record name
The «Record name» box on a number: before waiting at a meeting's door the caller hears «after the tone, please say your name» and speaks for up to 3 seconds. Hosts see in the waiting list that a name was recorded and can play it. The recording is kept only while the caller waits (at most «Wait at the door» + 2 minutes), never in the database, and is deleted as soon as they are let in. In the participant list the row stays the (masked) number, marked «(name recorded)». The organiser is not asked for a name.
Missed calls
When a call to a person is not answered, is declined, or the person is in Do Not Disturb, they get
a message: in their chat with the bot named in «Missed calls are written by the bot», or, with no
bot, a notice in the app. It carries the number (masked, or in full with «masked; in full in
missed-call messages»), the time and a «call back» button — which goes through the same checks as
any dial-out (calls:pstn, the plan, the quotas) and does not unmask a masked number.
Voice prompts
The gateway's prompts are English or Russian, chosen per number («Voice prompts»). The voices are synthesised in advance (English — Piper, Russian — Yandex SpeechKit; the licences are recorded in the repository), and only the recordings ship — the gateway calls out to nothing during a call.
What inbound does not have yet:
- sentences while waiting at the door — that is a tone;
- voicemail;
Next
- Calls and conferences — the media server, the lobby, recording
- Sizing