Owner asked for total minutes, call time and missed calls in one place, and the money left on voip.ms. Reports now totals answered calls, talk time and outside minutes (in from arrival, out from the answer: roughly what the provider bills), charts outside minutes per day, and shows each voip.ms trunk's balance (getBalance; verify with a real account).
9.9 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. - M3 acceptance check (verify): run
pjsip set logger on, call the DID from a mobile phone, and confirm which header carries the DID. Record the finding here. - 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.