Multi-tenant SIP topology: one Twilio Elastic Trunk per subaccount, or a shared setup?

Multi-tenant voice AI on LiveKit Cloud (Python Agents SDK), ~10 tenants today, ~100 within a year. Looking for a recommendation on trunk topology for inbound and outbound.

Constraint: each tenant’s PSTN numbers live in that tenant’s own Twilio subaccount. On Twilio the subaccount is the trunk-ownership boundary, an Elastic SIP Trunk can’t be shared across subaccounts, a number only attaches to a trunk in its owning subaccount, and outbound caller-ID must be authorized on a trunk in that subaccount. So a single shared Twilio trunk across tenants isn’t possible.

Two options:

Option A, one Twilio Elastic SIP Trunk per subaccount, both directions.

  • Outbound: dispatch the agent into the room, then create_sip_participant with an inline SIPOutboundConfig pointed at that tenant’s Twilio trunk hostname.
  • Inbound: the Twilio trunk’s OriginationUrl points straight at our LiveKit SIP host, terminating on a shared LiveKit inbound trunk gated by numbers + allowedAddresses (Twilio signaling IPs). No webhook in the audio path.

Option B, current setup, shared LiveKit inbound trunk plus a Twilio Programmable Voice TwiML bridge. Each number’s voiceUrl hits our webhook, which returns TwiML dialing our LiveKit SIP host. No per-subaccount trunk to provision, but it puts our webhook on the audio path and adds post-pickup latency outbound.

For this shape, where each tenant is in its own Twilio subaccount, which topology would you recommend at ~100 tenants? Is a per-subaccount Elastic Trunk (Option A) the pattern you’d expect, or is there a better approach?

Thanks!

How many phone numbers per tenant? Also, how are your agents architected? Is it one agent for all tenants or an agent per tenant?

We can have multiple numbers per tenant, avg ~10. Regarding the architecture, it’s one agent for all tenants.

So I think the architecture where you have inbound/outbound trunk per tenant is the way I would work it.

@Douglas_Rocha, Per-tenant trunks is the right call. The operational detail that decides your inbound design specifically: with one agent serving all tenants, the agent identifies the tenant from SIP participant attributes. sip.trunkPhoneNumber carries the dialed number (the tenant’s number on inbound) and sip.trunkID carries the inbound trunk used [ SIP participant | LiveKit Documentation ].

Per-tenant LK inbound trunks give you sip.trunkID-based tenant routing directly, plus per-tenant dispatch rules and allowedAddresses isolation. Your proposed shared LK inbound trunk works, but then the agent has to map sip.trunkPhoneNumber to a tenant via a number lookup, and you maintain one growing number list plus the Twilio signaling IP allowlist on a single trunk.

At 100 tenants, per-tenant inbound trunks is more provisioning but cleaner isolation and routing. For outbound, stored per-tenant outbound trunks scale better than inline SIPOutboundConfig when you’re managing 100 of them.