# Integrating Elastic SIP Trunking with LiveKit This guide explains how to connect self-hosted LiveKit to Sinch Elastic SIP Trunking (EST) for inbound and outbound calling. This guide assumes you have the following: * Static inbound routing from Elastic SIP Trunking to LiveKit. * ACL-based or digest-based authentication for outbound calling from LiveKit to Elastic SIP Trunking. ## Before you start Before you begin setting up your Sinch Elastic SIP trunk make sure you have done the following: * Sign up for a free account at https://www.sinch.com/ and request access for Elastic SIP Trunking. Need help? Click [here](https://community.sinch.com/t5/Customer-Dashboard/How-to-sign-up-for-your-free-Sinch-account/ta-p/8058) for instructions. * If you have not already done so, configure and create your Elastic SIP trunk. Instructions on how to do this can be found [here](https://community.sinch.com/t5/Elastic-SIP-Trunking/Getting-started-with-Elastic-SIP-Trunking/ta-p/11380). * Ensure you note the fully qualified domain name (FQDN) that was created on your SIP Trunk. You will need this while setting up your IP-PBX or SIP Application. * You have a phone number assigned to your SIP Trunk. * You have CLI access to the LiveKit host. * The `lk` CLI is installed. After setting up your Elastic SIP Trunk infrastructure, you can validate it using these [steps](/docs/est/test-plan). For first-time validation, a public Linux host is recommended because it usually reduces local firewall, NAT, and RTP troubleshooting issues. After changing EST ACLs, endpoints, or similar routing settings, wait at least 60 seconds before retesting. Open these ports on the LiveKit host: * `5060/UDP` for SIP signaling * `10000-20000/UDP` for SIP RTP * `7880/TCP` for the LiveKit API and WebSocket * `7881/TCP` for LiveKit TCP ICE fallback * `50000-60000/UDP` for LiveKit media If you plan to use TLS later, also expose `5061/TCP`. ### What this validates The quick start validates three milestones: 1. EST can deliver inbound SIP calls to self-hosted LiveKit. 2. LiveKit can place outbound SIP calls to EST. 3. After SIP is working, LiveKit can dispatch an agent into the inbound room and return audio to the caller. Treat these as separate stages. Do not start with agent debugging. ## Quick start Use this section for the fastest path to a working inbound and outbound test. ### 1. Configure Sinch EST On the Sinch EST side: 1. Create or reuse an EST trunk. 2. Assign a DID to that trunk. 3. In the Sinch Build dashboard, add a static endpoint under the trunk's inbound settings that points to your LiveKit SIP listener. 4. Add an ACL that allows your LiveKit public IP for outbound calls from LiveKit to EST. Example EST values: | | trunk domain | `YOUR_EST_TRUNK_DOMAIN` | | static endpoint target | `YOUR_LIVEKIT_PUBLIC_HOST:5060/UDP` | | assigned DID | `YOUR_EST_DID` | | ACL entry | `YOUR_LIVEKIT_PUBLIC_IP/32` | ### 2. Configure LiveKit CLI access ```bash export LIVEKIT_URL=http://YOUR_LIVEKIT_HOST:7880 export LIVEKIT_API_KEY=devkey export LIVEKIT_API_SECRET=devsecretdevsecretdevsecretdevsec lk room list ``` If `lk room list` returns successfully, your CLI access is working. Note: Current LiveKit releases reject very short sample API secrets, so use a sufficiently long secret. ### 3. Create LiveKit inbound routing Create the LiveKit inbound trunk for your EST DID: ```bash lk sip inbound create \ --name sinch-est-inbound \ --numbers YOUR_EST_DID ``` Save the returned `SIPTrunkID` as `YOUR_INBOUND_TRUNK_ID`. Create a dispatch rule that creates a room for each inbound caller: ```bash lk sip dispatch create \ --name sinch-est-caller-dispatch \ --trunks YOUR_INBOUND_TRUNK_ID \ --caller est-inbound- \ --randomize ``` Expected room pattern: ```text est-inbound-__ ``` ### 4. Test inbound calling Call `YOUR_EST_DID`. Expected result: * EST delivered the INVITE to LiveKit SIP * LiveKit matched the inbound trunk * LiveKit matched the dispatch rule * LiveKit created a room * the SIP caller joined as a LiveKit participant Useful success indicators: * `SIP participant joined room` * `Waiting for track subscription(s)` If you see those messages, inbound SIP is working even if no agent is attached yet. ### 5. Create LiveKit outbound routing This example assumes IP-based ACL authentication on the EST side. If you are using digest authentication instead, configure the EST trunk for digest auth and create the LiveKit outbound trunk to match that method. ```bash lk sip outbound create \ --name sinch-est-outbound \ --address YOUR_EST_TRUNK_DOMAIN \ --transport UDP \ --numbers YOUR_EST_CALLER_ID ``` Save the returned `SIPTrunkID` as `YOUR_OUTBOUND_TRUNK_ID`. ### 6. Test outbound calling ```bash lk sip participant create \ --room est-outbound-test \ --identity outbound-test \ --trunk YOUR_OUTBOUND_TRUNK_ID \ --number YOUR_EST_CALLER_ID \ --call YOUR_TEST_DESTINATION \ --wait \ --timeout 45s ``` Expected result: * EST accepted the outbound INVITE * LiveKit established the SIP call * RTP flowed successfully Useful success indicators: * `Outbound SIP call established` * `accepting RTP stream` ## How EST maps to LiveKit | | Inbound/Orgination | EST trunk + DID + static endpoint -> LiveKit inbound trunk + SIP dispatch rule | | Outbound/Termination | EST trunk domain + ACL or digest auth -> LiveKit outbound trunk + SIP participant create request | ## Next steps After you have confirmed a working system, the following next steps can extend your solution. ### Optional agent test Add an agent after plain SIP inbound and outbound tests pass. #### Recommended pattern Attach the agent through `roomConfig.agents` on the SIP dispatch rule. This is the cleanest pattern for inbound telephony because the room is created by the SIP dispatch rule and the agent is dispatched immediately when the room is created. #### Simple Gemini smoke test Gemini is a convenient optional smoke test because getting a key is relatively easy and basic testing is low-friction, but other models can easily substitute. #### Minimal Gemini example Example runtime env file: ```env LIVEKIT_URL=ws://YOUR_LIVEKIT_HOST:7880 LIVEKIT_API_KEY=devkey LIVEKIT_API_SECRET=devsecretdevsecretdevsecretdevsec GOOGLE_API_KEY=YOUR_GOOGLE_API_KEY ``` Example dependency set: ```text livekit-agents[google]~=1.5 python-dotenv>=1.0,<2.0 ``` Minimal agent shape used during validation: ```python from livekit import agents from livekit.agents import Agent, AgentServer, AgentSession, JobContext from livekit.plugins import google class InboundAssistant(Agent): def __init__(self) -> None: super().__init__(instructions="You are a concise phone assistant.") server = AgentServer() @server.rtc_session(agent_name="inbound-agent") async def inbound_agent(ctx: JobContext) -> None: session = AgentSession( llm=google.realtime.RealtimeModel( model="gemini-2.5-flash-native-audio-preview-12-2025", voice="Puck", instructions="Greet the caller and confirm audio is working.", ), ) await session.start(room=ctx.room, agent=InboundAssistant()) await session.generate_reply( instructions="Answer the call and ask whether the caller can hear you." ) if __name__ == "__main__": agents.cli.run_app(server) ``` Conceptual dispatch update: ```json { "roomConfig": { "agents": [ { "agentName": "inbound-agent" } ] } } ``` Expected result: * the agent worker registered successfully * the inbound SIP call triggered an agent job * the agent joined the room * the SIP side subscribed to the agent audio track * the caller heard the agent and the agent responded ### Optional TLS and SRTP Treat TLS and SRTP as an optional step after the basic UDP flow is working. For EST, TLS and SRTP go together. If you switch this integration to TLS, configure it as a TLS plus SRTP setup rather than enabling TLS signaling alone. At a minimum, confirm: * your LiveKit SIP listener is configured for TLS * EST is pointing its inbound static endpoint to your TLS listener, typically `YOUR_LIVEKIT_PUBLIC_HOST:5061/TLS` * your certificate configuration is valid * your LiveKit inbound and outbound SIP trunk configuration requires media encryption so SRTP is used * your outbound LiveKit trunk transport matches the EST-side TLS configuration If signaling connects over TLS but media does not establish, re-check the media-encryption settings on both LiveKit SIP trunks before troubleshooting deeper. ### Optional SIP REFER transfer LiveKit cold transfer integrates with EST via SIP `REFER`. If you plan to use cold transfers, see: * [SIP REFER](https://community.sinch.com/t5/Elastic-SIP-Trunking/How-do-I-use-call-transfers-with-SIP-REFER/ta-p/19346) * [https://docs.livekit.io/telephony/features/transfers/cold/](https://docs.livekit.io/telephony/features/transfers/cold/) ## Troubleshooting The following sections provide troubleshooting tips for issues you may encounter. ### No inbound INVITE reaches LiveKit Check: * The EST static endpoint IP or FQDN. * `5060/UDP` reachability. * Whether the public host IP changed. * Whether EST is pointing to the intended trunk endpoint. ### The room is never created Check: * The LiveKit inbound trunk contains the called DID. * The SIP dispatch rule references the correct LiveKit trunk ID. * The dispatch rule type matches your intended routing pattern. ### The room is created but there is no audio If no agent is attached, this is expected. If an agent is attached, check for: * `received job request` * The agent joining the room. * `track subscribed` * `mixing track` * `accepting RTP stream` ### The room is created but the agent never receives a job Check: * The dispatch rule includes the intended agent in `roomConfig.agents` if you are using named dispatch. * The running worker is registered with the same agent name expected by the dispatch rule. * You do not have a mismatch between named dispatch and automatic dispatch modes. Typical symptoms: * Rooms are created successfully. * SIP logs show the call was routed. * Agent logs never show `received job request`. * Callers hear silence or the call times out. ### The agent does not start or exits immediately Check: * `LIVEKIT_URL`, `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET` are set correctly. * Any provider key such as `GOOGLE_API_KEY` is set if you are using that provider. * LiveKit is reachable before starting the agent. * The agent process is not failing on startup with a missing environment variable or connection error. Typical symptom: * Errors similar to `ws_url is required`. * The agent starts, then exits immediately. ### Port `8081` is already in use Many LiveKit agent examples expose an HTTP listener on `8081`. If startup fails with an address-in-use error, stop the old agent process and restart the intended one. ### Outbound call fails Check: * The EST authentication method * Whether your EST trunk is using IP-based ACL auth or digest auth. * The EST ACL contents if using IP-based auth. * The outbound trunk address and transport. * Caller ID formatting in E.164. * Whether EST accepts that caller ID on the trunk. If UDP appears to be blocked or unreliable, try TCP as a troubleshooting step. ### One-way audio Check: * `10000-20000/UDP` for SIP RTP * `50000-60000/UDP` for LiveKit media * Host NAT and firewall behavior. * Whether `use_external_ip: true` is set where needed. ### CLI output looks misleading During testing, some `lk sip ... list` output was less reliable than isolated commands and raw `--curl` verification. If output looks wrong: * Rerun the command by itself. * Use `lk --curl ...`. * Inspect service logs directly. ### Repeated health-check or monitoring calls affect agent availability If you see repeated inbound SIP activity without real callers, monitoring or health-check traffic may be creating rooms and consuming worker capacity. Typical symptoms: * Agent workers appear busy before a real call arrives. * Real callers hit `no agents available` behavior. * SIP logs show repeated incomplete inbound calls. If this happens, review worker capacity settings and confirm which inbound calls are real traffic versus monitoring traffic. ### Useful logs For Docker-based self-hosting: ```bash docker logs livekit-sip-1 docker logs livekit-livekit-1 ``` Useful success indicators: * `SIP participant joined room` * `Outbound SIP call established` * `track subscribed` * `mixing track` * `accepting RTP stream` * `received job request` * `registered worker` ## Deployment reference Use the following examples as a minimal self-hosted reference deployment. ### Example `livekit.yaml` ```yaml port: 7880 bind_addresses: - "0.0.0.0" rtc: tcp_port: 7881 port_range_start: 50000 port_range_end: 60000 use_external_ip: true redis: address: 127.0.0.1:6379 keys: devkey: devsecretdevsecretdevsecretdevsec ```