The outside install, restores, the week on the owner's server, Portainer updates and both registries are done. The mobile app is after v1.
10 KiB
06 — voip.ms trunk, numbers, SMS and E911
voip.ms is the reference provider. Everything here also has a "generic" SIP trunk fallback (same fields, no voip.ms-specific help text or API features).
Verify-before-trusting rule: voip.ms changes its portal and API over time. Every item marked (verify) must be checked against the current voip.ms wiki/API docs or a real packet capture during M3/M9, and this file updated.
1. What the customer does in the voip.ms portal (shown as a checklist in the UI)
- Create a sub-account (Main Menu → Sub Accounts → Create). Use one sub-account per PBX, e.g.
123456_pbx.- Authentication type: User/Password (or IP auth if the server has a static IP)
- Password: strong, random
- Device type: Asterisk, IP PBX, Gateway or VoIP Switch (verify the label)
- Allowed codecs: G.711u (ulaw). Others optional.
- DTMF mode: RFC 2833
- NAT: Yes
- Encrypted SIP traffic (TLS/SRTP): optional. If on, set our trunk transport to TLS, port 5061, SRTP on. Needs Settings → Security → Accept SIP over TLS (a certificate in
data/tls, 08 §2.1) first. voip.ms's certificate names every POP, so the trunk checks it (verify_server). - CallerID Number: your main DID.
- International calling: leave disabled unless needed (toll-fraud protection on their side too).
- Choose a POP server close to the PBX (e.g.
chicago1.voip.ms). The sub-account registers to this server. - Point each DID at the sub-account (DID Numbers → Manage DIDs → edit):
- Routing: SIP/IAX → the sub-account
- POP: the same server the sub-account registers to. A mismatch is a common cause of inbound calls failing (verify).
- E911: register a street address for every DID used as an emergency caller ID (DID → e911). See §6.
- (Optional) API access for SMS and DID import: Main Menu → SOAP and REST/JSON API → enable, set an API password, and whitelist the PBX's public IP.
- (Optional) SMS: on each DID, enable SMS/MMS and set the SMS URL Callback (§5.2).
- Balance protection: in voip.ms account settings, set low-balance alerts and per-call/daily limits where offered.
2. Trunk form (voip.ms preset)
| Field | Default | Notes |
|---|---|---|
| Name | voip.ms |
|
| Server | dropdown | Filled from API getServersInfo when API creds are set; otherwise free text with examples chicago1.voip.ms, atlanta1.voip.ms, dallas1.voip.ms, newyork1.voip.ms, losangeles1.voip.ms, seattle1.voip.ms, montreal1.voip.ms, toronto1.voip.ms (verify names) |
| Auth mode | registration | |
| Username | sub-account, format ^\d+_[A-Za-z0-9_]+$ or the 6-digit main account |
|
| Password | ||
| Transport | UDP 5060 | TLS 5061 when encryption is enabled on the sub-account |
| Codecs | ulaw | |
| Max channels | empty | |
| API username / password | empty | The API username is the account email |
The generated PJSIP config is in 03-config-generator.md §5.3. It follows the
voip.ms Asterisk PJSIP wiki: registration + auth + aor + endpoint + identify,
rtp_symmetric, rewrite_contact, send_rpid and from_user = account.
3. Inbound calls: which DID was dialed?
- voip.ms's Asterisk example matches inbound calls on
exten => <10-digit DID>, which means the Request-URI user is the 10-digit DID (no leading 1). - The dialplan (03 §6.4) normalizes it to 11 digits and checks
sp-did-exists. If that fails, it falls back to the user part of theTo:header. That covers either behavior. - Checked (owner, 2026-10-21): a mobile → DID call came in with the DID as the Request-URI
user, and the dialplan routed it to the digital receptionist. The
To:fallback stays for other providers. - Inbound caller ID arrives as 10 digits for North American callers. The dialplan normalizes it to 11.
4. Outbound calls
- voip.ms accepts
1NXXNXXXXXX(11 digits) and011…for international. Our routes always send 11 digits. - Caller ID: we set
CALLERID(num), andsend_rpid=yessends it inRemote-Party-ID. (verify) whether voip.ms honors a per-call caller ID that is one of the account's DIDs, or always uses the sub-account's CallerID setting. If it always overrides: show a notice on the extension "Outbound caller ID" field ("voip.ms uses the sub-account caller ID; create one sub-account per caller ID if you need different ones") and, as a workaround, allow several trunks (one per sub-account) with routes that choose the trunk by caller. - Toll-free and NANP routes are created by the setup wizard (03 §6.12). International is created disabled.
5. SMS / MMS
5.1 Sending (voip.ms REST API)
integrations/voipms/client.ts:
GET https://voip.ms/api/v1/rest.php
?api_username=<email>&api_password=<api pw>&method=sendSMS
&did=<10-digit DID>&dst=<10-digit number>&message=<text>
- Response JSON
{"status":"success","sms":<id>}. Any otherstatusis an error code string. Store it insms_messages.error. - Messages over 160 characters: use
method=sendMMS(messageup to 2048 chars (verify limit)), plusmedia1…media3URLs for pictures. We split nothing ourselves. - Numbers: voip.ms wants 10 digits for NANP, so strip the leading
1. - The API password goes in a query string, so never log the request URL.
- Rate limit our sends to 1 per second per DID (queue in memory).
- Other methods used:
getDIDsInfo(DID import),getServersInfo(server dropdown),getSMS(optional polling fallback). All behindvoipmsApi(method, params)with a 15 s timeout. getBalance(owner, 2026-10-21: the balance on Reports):balance.current_balance, a string like"12.3456"in the account's currency (verify the shape and that the API login needs nothing more, with a real account). Each Reports load asks once per voip.ms trunk that has an API login.
5.2 Receiving
Endpoint: GET|POST /hooks/voipms/sms/:token. :token is a 32-char random value stored
in settings key sms.webhookToken (shown in the DID's SMS settings with a copy button).
The full URL to paste into voip.ms is shown in the UI.
voip.ms offers two callback styles (verify both):
- URL callback (GET) with placeholders. The UI tells the user to paste:
https://<host>/hooks/voipms/sms/<token>?from={FROM}&to={TO}&message={MESSAGE}&id={ID}&date={DATE} - Webhook (JSON POST). Accept any JSON body. Parse the known shape (look for
data.payload.from.phone_number,data.payload.to[0].phone_number,data.payload.text,data.payload.media[].url,data.payload.id). If the shape doesn't match, store the raw body in the log atwarnlevel (text redacted to its length) and return 200 so voip.ms doesn't retry forever. M9 task: capture one real payload intotest/fixtures/voipms/sms-webhook.jsonand write the parser test from it.
Handler: normalize numbers, find the DID (404 if unknown), insert sms_messages
(direction='in', status='received'), dedupe on provider_id, broadcast
sms.received to admins + did_sms_users, fire the webhook. Reply ok (text/plain, 200).
Callback IPs: voip.ms doesn't publish a fixed list (verify), so security is the secret token.
Rate limit the endpoint to 60 requests/min.
6. Emergency calling (E911): US rules for a PBX
Legal background (summary, not legal advice; the admin guide must say "check with counsel"):
- Kari's Law (US, 47 CFR 9.16): users of a multi-line phone system must be able to dial 911 directly, with no prefix like 9, and the system must notify a central location (front desk, security, admin) when 911 is dialed, where configured.
- RAY BAUM'S Act (47 CFR 9.16(b)): the 911 call must convey a dispatchable location (street address plus room/floor where needed).
How SelfPhoneNG meets this:
911and9911are always dialable from every extension with no restrictions (03 §6.14). Unit test: an extension withoutbound_class = internal, DND on, and every trunk at max channels can still reach the 911 Dial line.- Notifications: email + phone ring-out + dashboard banner + webhook (04 §5.3).
- Location: voip.ms delivers the address registered on the DID used as caller ID.
- Every extension can have its own
emergency_caller_id_did_id(a DID whose e911 address matches that person's location). Otherwise the system defaultemergency.defaultCallerIdDidIdis used. extensions.emergency_location(e.g. "2nd floor, room 210") is put into the notification email. The PSAP can't receive it through voip.ms. The admin guide must explain this limitation, and that remote workers need a DID with their own registered address (or must call 911 from a mobile phone).
- Every extension can have its own
- Warnings: the admin dashboard shows a red banner while any trunk exists but
emergency.defaultCallerIdDidIdis unset, or while that DID'se911_addressfield is empty ("Confirm this DID has an E911 address at voip.ms"). - Never place a real 911 test call. Settings → Emergency → "Test notifications" runs steps 2–4
of 04 §5.3 with the subject prefixed
TESTand dials nothing. (Some carriers offer a test number; check voip.ms docs before adding one.) - Canada: 911 works the same way through voip.ms. The admin guide links to voip.ms's Canadian e911 info.
7. DID import (POST /api/trunks/:id/import-dids)
Calls getDIDsInfo. Shows a list with checkboxes in the UI. Imported DIDs get
destination {type:'hangup'} and a warning chip "Not routed yet" until edited.
As built (M9-T1): integrations/voipms/client.ts. Assumed shapes (verify with a real account,
voip.ms's API docs need a login): getServersInfo → servers[].server_hostname; getDIDsInfo →
dids[].did (10 digits), .description, .sms_enabled ("1"); status no_did = no numbers;
invalid_credentials, ip_not_enabled get their own messages. "Import numbers from voip.ms" is in
the trunk form's API section and the setup wizard's numbers step; the label comes from the DID's
description. The server list falls back to the built-in one when the API can't be reached.