Skip to content
TriedDocs
Telephony

Custom telephony and SIP trunking

Connect numbers from any carrier or PBX to Tried over SIP, what Tried provisions for each number, and how to test the connection.

Tried answers and places calls over SIP through LiveKit SIP. Any carrier or PBX that can send SIP to a hostname can bring its numbers: Twilio Elastic SIP Trunking, Telnyx, your own carrier, or an on-premise PBX. You keep the number with your carrier; Tried only needs to receive its calls and to send outbound calls back through your trunk.

There are three ways to connect:

Method Use when How
Import a number Each number has its own trunk settings Connect via SIP trunking, or POST /import-phone-number
Shared SIP trunk Many numbers on one carrier trunk POST /create-sip-trunk, then import numbers with sip_trunk_id (API only)
Register call Your PBX decides per call which agent answers POST /v2/register-phone-call. See Register call.
Needs keys

Real SIP needs LiveKit: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET and LIVEKIT_SIP_URI (the SIP host your carrier dials, for example <project>.sip.livekit.cloud or your own livekit-sip hostname), plus the LiveKit webhook at https://<api>/internal/livekit/webhook. Without them, provisioning is recorded in the provider operation log and nothing is sent.

The two directions#

Inbound (origination). Your carrier sends calls for the number to Tried. Point the number's route at:

sip:<number>@<LIVEKIT_SIP_URI>

GET /sip-origination returns the values for your deployment: sip_host, origination_uri (sip:<host>;transport=tcp) and ip_ranges, the address ranges our SIP traffic comes from, for your trunk's allow list.

Outbound (termination). Tried sends calls to your carrier's termination URI, authenticated with a SIP username and password if your carrier needs them. The caller ID is the number itself.

Import a number#

Open the dialog#

On Phone Numbers, click + and Connect via SIP trunking.

Enter the number and trunk#

Phone Number in E.164. Termination URI is your carrier's SIP host, for example sip.provider.com, optionally with a port. Choose Transport (TCP, UDP or TLS) and Media encryption.

Test the trunk#

Click Test SIP connection. Tried sends one SIP OPTIONS request to the termination URI and shows whether it answered, the status and the round trip time.

Add credentials and options#

SIP username and SIP password for outbound calls. Optional: Fallback number, Calls per second (up to 150 on your own trunk), Nickname (optional) and Inbound agent (optional).

Save#

Click Save. The number is provisioned on LiveKit SIP.

Route the number at your carrier#

Send the number's inbound calls to sip:<number>@<LIVEKIT_SIP_URI>.

Connect number via SIP trunking dialog

Import with the API#

curl -X POST https://api.tried.com.au/import-phone-number \
  -H "Authorization: Bearer $TRIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+61290001234",
    "termination_uri": "sip.carrier.example.com",
    "sip_trunk_auth_username": "tried",
    "sip_trunk_auth_password": "a-strong-password",
    "transport": "TLS",
    "media_encryption": "required",
    "allowed_inbound_addresses": ["203.0.113.10/32"],
    "inbound_agents": [{ "agent_id": "agent_8e0c...", "weight": 1 }]
  }'
Field Type Default Description
phone_number string required E.164.
termination_uri string required unless sip_trunk_id Carrier SIP host, optionally host:port.
sip_trunk_auth_username string none Username for outbound calls.
sip_trunk_auth_password string none Password for outbound calls. Stored encrypted (AES-GCM) and never returned.
transport TCP | UDP | TLS TCP Signalling transport.
media_encryption disabled | allowed | required allowed SRTP. allowed uses SRTP when the other side offers it; required refuses plain RTP.
allowed_inbound_addresses string[] none IPs or CIDRs allowed to send INVITEs for this number.
sip_trunk_id string none Put the number on a shared workspace trunk instead.
provider custom | twilio detected twilio is set automatically when the termination URI is a *.pstn.twilio.com host.
fallback_number E.164 none Where inbound calls go when no slot frees up.
cps_limit number, 0.1 to 150 10 Outbound calls per second.
nickname, inbound_agents, outbound_agents, inbound_webhook_url, allowed_inbound_country_list, allowed_outbound_country_list Same as on bought numbers.

The response is HTTP 201 with the number. Imported numbers have provider: "custom" (or "twilio").

What Tried provisions#

Importing, buying, updating or deleting a number creates, updates or deletes its LiveKit objects through the LiveKit server SDK. You never run lk sip commands.

Object Contents
Inbound trunk The number, allowedAddresses from allowed_inbound_addresses, media encryption, Krisp noise cancellation, all X- headers plus User-to-User, Diversion, History-Info and P-Asserted-Identity mapped to attributes, and BYE header mapping
Outbound trunk Address from the termination URI, transport, username and password, media encryption
Dispatch rule One room per call with the prefix call-, and the attributes tried.phone_number and tried.workspace_id

The number records the IDs in livekit_inbound_trunk_id, livekit_outbound_trunk_id and livekit_dispatch_rule_id, and the result in provisioning_status:

Status Meaning
pending Created, provisioning not finished
provisioned All objects exist
error Something failed; provisioning_error has the message

POST /provision-phone-number/:n re-applies everything. Changing termination_uri, credentials, transport, media_encryption, allowed_inbound_addresses or sip_trunk_id with PATCH /update-phone-number/:n replaces the trunks in place.

Shared SIP trunks#

When many numbers use the same carrier trunk, create one shared trunk and attach the numbers to it. One inbound trunk, one outbound trunk and one dispatch rule serve all of them. This is API only; the dashboard imports one number at a time.

curl -X POST https://api.tried.com.au/create-sip-trunk \
  -H "Authorization: Bearer $TRIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Head office PBX",
    "termination_uri": "pbx.example.com:5061",
    "transport": "TLS",
    "media_encryption": "allowed",
    "allowed_inbound_addresses": ["203.0.113.10/32"],
    "inbound_auth_username": "pbx",
    "inbound_auth_password": "another-strong-password"
  }'
Field Default Description
name SIP trunk Label.
termination_uri required Outbound target.
sip_trunk_auth_username, sip_trunk_auth_password none Outbound credentials.
transport TCP TCP, UDP or TLS.
media_encryption allowed disabled, allowed or required.
allowed_inbound_addresses [] IPs or CIDRs allowed to send INVITEs.
inbound_auth_username, inbound_auth_password none Digest credentials your carrier must present on inbound INVITEs.

Then import each number with sip_trunk_id and no termination_uri. The trunk's number list stays in sync. When the last number leaves the trunk, its LiveKit objects are removed, because an inbound trunk with no numbers would catch every call. Other endpoints: GET /list-sip-trunks, GET /get-sip-trunk/:id, PATCH /update-sip-trunk/:id, DELETE /delete-sip-trunk/:id.

Test the SIP connection#

  • POST /test-sip-connection with { termination_uri, transport } tests a trunk before you import.
  • POST /test-phone-number-connection/:n tests a saved number (its trunk, its shared trunk, or sip.telnyx.com for Telnyx numbers).

Both send one SIP OPTIONS over UDP, TCP or TLS and return { reachable, status_code, reason, latency_ms }. In mock mode the result is simulated (200 OK (mock)) and the dashboard says (simulated, no SIP server configured). The same test is on each number's Routing and SIP card.

Codecs and media#

LiveKit SIP negotiates PCMU, PCMA and G.722. Caller keypad presses arrive as RFC 2833 telephone events, and the agent's press_digit tool sends RFC 2833 events.

Local testing without keys#

scripts/sip-smoke.sh runs livekit-server and livekit-sip locally with the API and the worker, and places real SIP calls with a small SIP client (PCMU and RFC 4733 DTMF). No vendor keys or root access needed.