Trunks
A trunk is a SIP connection to a carrier. Tring stores trunks in Postgres and renders each active one to a FreeSWITCH gateway file, then asks FreeSWITCH to rescan its external profile. Changes take effect without restarting FreeSWITCH or dropping live calls.
Outbound calls leave on a trunk through an outbound number that references it with trunk_id. A trunk with no outbound number is never picked for outbound calls.
All routes on this page take a tenant API key or a console session. See Authentication.
Auth modes
Section titled “Auth modes”The register flag picks how the carrier authenticates you.
| Mode | register |
Fields used | Typical carrier |
|---|---|---|---|
| REGISTER | true |
sip_username, sip_password, sip_realm; optionally proxy_ip and extra_params.auth_username |
Aggregators and IMS carriers that issue SIP credentials. FreeSWITCH registers and keeps the registration alive: to the realm, or to proxy_ip when set. |
| IP-authenticated | false (default) |
proxy_ip, optionally sip_realm |
Private circuits where the carrier trusts your IP. No REGISTER, no credentials. Calls are sent to proxy_ip. |
auth_acl_cidr (the carrier’s SBC range, for example 198.51.100.8/29) is stored with the trunk for reference only. It is not rendered into FreeSWITCH. Allowing inbound INVITEs from that range is configured separately in FreeSWITCH’s ACL.
What gets rendered to FreeSWITCH
Section titled “What gets rendered to FreeSWITCH”Each active trunk becomes one file, t_<trunk-uuid>.xml, in the dynamic gateway directory (/etc/freeswitch/sip_profiles/external/dynamic by default, overridable with TRING_GATEWAY_DIR). Inactive and deleted trunks have their files removed. The gateway name is t_ plus the first 12 hex characters of the trunk UUID, for example t_9a4c6e803333.
A REGISTER trunk:
<?xml version="1.0" encoding="UTF-8"?><include> <gateway name="t_9a4c6e803333"> <param name="username" value="tring-trunk-01"></param> <param name="password" value="(from sip_password)"></param> <param name="realm" value="sip.example.com"></param> <param name="register" value="true"></param> <param name="expire-seconds" value="600"></param> <param name="retry-seconds" value="30"></param> <param name="ping" value="25"></param> <param name="caller-id-in-from" value="true"></param> </gateway></include>An IP-authenticated trunk with from_domain set:
<?xml version="1.0" encoding="UTF-8"?><include> <gateway name="t_4e1f0a2b4444"> <param name="register" value="false"></param> <param name="proxy" value="198.51.100.10"></param> <param name="realm" value="198.51.100.10"></param> <param name="from-domain" value="203.0.113.10"></param> <param name="ping" value="25"></param> <param name="caller-id-in-from" value="true"></param> </gateway></include>- Every value is written into FreeSWITCH config, where
${…}is expanded, so no field may contain${. Hosts (sip_realm,proxy_ip,from_domain) may only use host/IP characters (plus:portand;transport=…), and usernames onlyA-Z a-z 0-9 . _ @ + -. A trunk stored before these checks that breaks them is left out of FreeSWITCH (logged) until it’s fixed. - In IP mode,
realmissip_realm, orproxy_ipwhensip_realmis empty. - In REGISTER mode,
realmissip_realm, orproxy_ipwhensip_realmis empty.proxy_ip(when set) adds<param name="proxy">: REGISTER and calls go to that address whilerealmstays the SIP domain. Use it when the realm isn’t resolvable from your network, as with a carrier’s IMS domain.extra_params.auth_usernameadds<param name="auth-username">. pingmakes FreeSWITCH send SIP OPTIONS every 25 seconds. That’s how an IP-auth trunk’sstatusis known (see below).caller-id-in-fromputs each call’s caller ID (the outbound number’s DID) in theFromuser, so there’s no trunk-levelfrom-user.
extra_params
Section titled “extra_params”extra_params is a free-form JSON object stored with the trunk. Tring reads these keys from it:
| Key | Type | Rendered as | Use |
|---|---|---|---|
from_domain |
string | <param name="from-domain"> |
Sets the host part of From on calls, OPTIONS pings and REGISTER. Some carriers require your own circuit IP there rather than their realm. |
auth_username |
string | <param name="auth-username"> |
REGISTER mode: the digest username when it differs from sip_username. IMS carriers register a public identity (sip_username) but authenticate a private one (auth_username). |
Other keys are stored and returned but ignored. A missing or malformed value is simply left out of the XML.
The trunk object
Section titled “The trunk object”| Field | Type | Description |
|---|---|---|
id |
UUID | Trunk ID. |
tenant_id |
UUID | Owning tenant. |
name |
string | Your label. Unique within the tenant. |
provider_type |
string | Drives destination-number normalisation (see below). Default generic. |
sip_username |
string | REGISTER mode username. |
has_password |
boolean | true when a sip_password is stored. The password itself is write-only: no response ever includes it. With TRING_SECRETS_KEY set it is stored encrypted (AES-256-GCM) and decrypted only to write the FreeSWITCH gateway config; see Encryption at rest. |
sip_realm |
string | REGISTER realm, or the IP-mode realm override. |
register |
boolean | true = REGISTER auth, false = IP auth. |
proxy_ip |
string | IP mode: the carrier SBC to send calls to. REGISTER mode (optional): the registrar/SBC to send REGISTER and calls to. |
auth_acl_cidr |
string | The carrier’s SBC range, stored for reference. |
active |
boolean | Only active trunks are rendered to FreeSWITCH. |
extra_params |
object | See above. |
created_at |
timestamp | |
updated_at |
timestamp | |
status |
string | List responses only. Live health: online, offline, connecting, not_loaded or unknown. |
state_raw |
string | List responses only. FreeSWITCH’s raw view, for example REGED ping=UP 2.74ms or NOREG ping=DOWN. Omitted when FreeSWITCH has no such gateway. |
reg_state |
string | List responses only. Registration state: REGED (registered), NOREG (not registering, normal for IP auth), TRYING, FAIL_WAIT, NOAUTH, EXPIRED, … Omitted when FreeSWITCH has no such gateway. |
ping_ms |
number | List responses only. Last OPTIONS round trip in milliseconds. Omitted until a ping is answered. |
calls |
object | List responses only. Calls through this trunk since FreeSWITCH (re)loaded it: out, failed_out, since, plus in and failed_in for registered trunks only (FreeSWITCH doesn’t attribute inbound calls to IP-auth trunks). Omitted when FreeSWITCH has no such gateway. |
How status is worked out:
- REGISTER trunks: from the registration state.
REGEDisonline;TRYING/REGISTERisconnecting; failed or unregistered states areoffline. - IP-auth trunks: from the OPTIONS ping.
onlineneeds an answered ping with a measured round trip; up but not yet measured isconnecting; no answer isoffline. not_loadedmeans FreeSWITCH has no gateway by that name: the trunk is inactive, the gateway file wasn’t picked up, or the API couldn’t reach FreeSWITCH.
provider_type
Section titled “provider_type”provider_type describes how destination numbers should be normalised before dialing on this trunk:
| Value | Normalisation |
|---|---|
airtel, jio, tata |
Indian mobile numbers only. Accepts +91…, 91…, 0… and bare 10-digit forms, with spaces or dashes. Normalises to +91XXXXXXXXXX. Anything else is rejected with 400. |
generic (default), loopback, or any other value |
No normalisation. The trimmed input must be a bare number (optional leading +, 3 to 15 digits) and is dialed as is. |
The media gateway dials every outbound call today (POST /v1/calls, POST /v1/calls/connect, an IVR transfer to a number), and it always applies the Indian rules whatever the trunk’s provider_type. Until that changes, only Indian mobile numbers can be dialed.
POST /v1/trunks
Section titled “POST /v1/trunks”Create a trunk, then re-render every gateway file and rescan FreeSWITCH.
Auth: tenant API key or console session.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Label, unique within the tenant. |
provider_type |
string | no | Default generic. See the table above. |
register |
boolean | no | Default false (IP auth). |
sip_username |
string | REGISTER mode | |
sip_password |
string | REGISTER mode | |
sip_realm |
string | REGISTER mode | In IP mode, optional realm override. |
proxy_ip |
string | IP mode; optional in REGISTER mode | Carrier SBC address. In REGISTER mode, the registrar to send REGISTER and calls to. |
auth_acl_cidr |
string | no | Carrier SBC range, for reference. |
active |
boolean | no | Default true. |
extra_params |
object | no | Default {}. See extra_params. |
The API doesn’t enforce the per-mode fields. A REGISTER trunk with no credentials, or an IP trunk with no proxy_ip, is saved and rendered as is.
curl -sS -X POST "$TRING_URL/v1/trunks" \ -H "Authorization: Bearer $TRING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "carrier-a-primary", "provider_type": "tata", "register": false, "proxy_ip": "198.51.100.10", "auth_acl_cidr": "198.51.100.8/29", "extra_params": {"from_domain": "203.0.113.10"} }'For a REGISTER trunk, read the password from a secret store rather than typing it into a command:
curl -sS -X POST "$TRING_URL/v1/trunks" \ -H "Authorization: Bearer $TRING_API_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg pw "$CARRIER_SIP_PASSWORD" '{ name: "aggregator-b", provider_type: "generic", register: true, sip_username: "tring-trunk-01", sip_password: $pw, sip_realm: "sip.example.com" }')"Response: 201 Created with the trunk object (without status). sip_password is never returned; has_password shows whether one is set.
{ "id": "4e1f0a2b-4444-4a00-8d00-000000000001", "tenant_id": "7d3e9f10-0000-4000-8000-000000000001", "name": "carrier-a-primary", "provider_type": "tata", "sip_username": "", "has_password": false, "sip_realm": "", "register": false, "proxy_ip": "198.51.100.10", "auth_acl_cidr": "198.51.100.8/29", "active": true, "extra_params": {"from_domain": "203.0.113.10"}, "created_at": "2026-10-16T10:02:31.220114+05:30", "updated_at": "2026-10-16T10:02:31.220114+05:30"}Errors
| Status | Message |
|---|---|
400 |
invalid JSON body |
400 |
name is required |
400 |
sip_username may only contain letters, digits and . _ @ + - |
400 |
sip_password may not contain ${ or control characters |
400 |
sip_realm and proxy_ip must be a host or IP, optionally with :port |
400 |
extra_params.from_domain must be a host or IP / extra_params.auth_username may only contain … |
409 |
a trunk with this name already exists |
500 |
trunk insert failed |
502 |
trunk saved but FreeSWITCH did not reload — retry with POST /v1/trunks/reload. The trunk is saved; FreeSWITCH just hasn’t picked it up. |
GET /v1/trunks
Section titled “GET /v1/trunks”List your tenant’s trunks, sorted by name, with live health. Not paginated.
Auth: tenant API key or console session.
Query parameters: none.
Health comes from FreeSWITCH on each request. If FreeSWITCH can’t be reached, trunks show not_loaded rather than the request failing.
curl -sS "$TRING_URL/v1/trunks" \ -H "Authorization: Bearer $TRING_API_KEY"Response: 200 OK
[ { "id": "4e1f0a2b-4444-4a00-8d00-000000000001", "tenant_id": "7d3e9f10-0000-4000-8000-000000000001", "name": "carrier-a-primary", "provider_type": "tata", "sip_username": "", "has_password": false, "sip_realm": "", "register": false, "proxy_ip": "198.51.100.10", "auth_acl_cidr": "198.51.100.8/29", "active": true, "extra_params": {"from_domain": "203.0.113.10"}, "created_at": "2026-10-16T10:02:31.220114+05:30", "updated_at": "2026-10-16T10:02:31.220114+05:30", "status": "online", "state_raw": "NOREG ping=UP 2.74ms", "reg_state": "NOREG", "ping_ms": 2.74, "calls": {"out": 5, "failed_out": 5, "since": "2026-10-19T08:00:00Z"} }]Errors
| Status | Message |
|---|---|
500 |
query failed / scan failed |
GET /v1/trunks/{id}
Section titled “GET /v1/trunks/{id}”Fetch one trunk.
Auth: tenant API key or console session.
Unlike the list, this response has no status/state_raw. Like every trunk response, it carries has_password but never sip_password.
curl -sS "$TRING_URL/v1/trunks/4e1f0a2b-4444-4a00-8d00-000000000001" \ -H "Authorization: Bearer $TRING_API_KEY"Response: 200 OK with the trunk object.
Errors
| Status | Message |
|---|---|
400 |
invalid id |
404 |
trunk not found |
500 |
query failed |
PATCH /v1/trunks/{id}
Section titled “PATCH /v1/trunks/{id}”Update any trunk fields, then re-render and rescan.
Auth: tenant API key or console session.
Request body. Every field is optional; leave a field out to keep its current value.
| Field | Type | Description |
|---|---|---|
name |
string | |
provider_type |
string | |
sip_username |
string | |
sip_password |
string | New password. Left out or empty keeps the stored one, so you can update other fields without resending it. |
sip_realm |
string | |
register |
boolean | Switch auth mode. |
proxy_ip |
string | |
auth_acl_cidr |
string | |
active |
boolean | false removes the gateway from FreeSWITCH. |
extra_params |
object | Replaces the whole object. It is not merged. |
curl -sS -X PATCH "$TRING_URL/v1/trunks/4e1f0a2b-4444-4a00-8d00-000000000001" \ -H "Authorization: Bearer $TRING_API_KEY" \ -H "Content-Type: application/json" \ -d '{"active": false}'Response: 200 OK with the updated trunk object.
Errors
| Status | Message |
|---|---|
400 |
invalid id |
400 |
invalid JSON body |
400 |
sip_username may only contain letters, digits and . _ @ + - |
400 |
sip_password may not contain ${ or control characters |
400 |
sip_realm and proxy_ip must be a host or IP, optionally with :port |
400 |
extra_params.from_domain must be a host or IP / extra_params.auth_username may only contain … |
404 |
trunk not found |
409 |
a trunk with this name already exists |
500 |
update failed |
502 |
trunk updated but FreeSWITCH did not reload — retry with POST /v1/trunks/reload |
DELETE /v1/trunks/{id}
Section titled “DELETE /v1/trunks/{id}”Delete a trunk and remove its gateway file. Numbers that referenced it keep existing with trunk_id set to null. An outbound number with no trunk is skipped when picking routes.
Auth: tenant API key or console session.
curl -sS -X DELETE "$TRING_URL/v1/trunks/4e1f0a2b-4444-4a00-8d00-000000000001" \ -H "Authorization: Bearer $TRING_API_KEY"Response: 204 No Content.
Errors
| Status | Message |
|---|---|
400 |
invalid id |
404 |
trunk not found |
500 |
delete failed |
502 |
trunk deleted but FreeSWITCH did not reload — retry with POST /v1/trunks/reload |
POST /v1/trunks/reload
Section titled “POST /v1/trunks/reload”Re-render every active trunk’s gateway file and rescan FreeSWITCH’s external profile. Use it after a 502 from a trunk write, or whenever FreeSWITCH has drifted from the database (for example after a FreeSWITCH restart with a stale gateway directory).
This re-renders the gateways of every tenant on the deployment, not just yours. It’s safe to repeat: the rescan doesn’t drop live calls.
Auth: tenant API key or console session.
Request body: none.
curl -sS -X POST "$TRING_URL/v1/trunks/reload" \ -H "Authorization: Bearer $TRING_API_KEY"Response: 200 OK
{ "status": "reloaded" }Errors
| Status | Message |
|---|---|
500 |
reload failed. The file-write or FreeSWITCH error is in the API server log. |