should outbound trunks exist per organization or p...
# ask-ai
l
should outbound trunks exist per organization or per voice agent ?
t
Thanks for reaching out, Anirban. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking whether outbound trunks should be created per organization or per voice agent. Outbound trunks are configured at the organization/project level, not per voice agent. A single outbound trunk (with its SIP credentials, provider endpoint, region settings, etc.) can be reused by multiple agents and workflows. For example, you can pass the same
sip_trunk_id
when creating SIP participants for different agents. Would you like guidance on how to share a trunk across agents or an example of creating a SIP participant with a trunk? Sources: Making calls overview | LiveKit docs | Python SDKs | livekit/livekit_composite | DeepWiki | sip | livekit/livekit_composite | DeepWiki | https://blog.livekit.io/introducing-livekit-phone-numbers-zero-to-ringing-in-60-seconds/#/portal/signup| | Outbound Calling Script | LiveKit docs
l
and what about inbound trunks ?
t
You're asking whether inbound trunks should be per organization or per voice agent. Inbound trunks are configured at the organization/project level, not per agent. A single inbound trunk can receive calls for multiple phone numbers and be shared across agents and workflows. Routing to specific agents is handled by dispatch rules or room configuration (for example,
roomConfig.agents
or specifying an
agentName
), so one trunk can route different incoming calls to different agents or rooms. LiveKit also offers LiveKit Phone Numbers, which are managed at the project level and use dispatch rules in the same way. Would you like an example dispatch rule or steps to route calls to a specific agent? Sources: sip | livekit/livekit_composite | DeepWiki | Agents telephony integration | LiveKit docs | https://blog.livekit.io/introducing-livekit-phone-numbers-zero-to-ringing-in-60-seconds/#/portal|
l
for multiple agents in an organization with outbound calls support should i designate phone numbers to call to each agnet or at org level
t
You're asking whether to assign phone numbers per agent or at the organization level for outbound calls. Assign numbers at the organization/project level via outbound trunks. Outbound trunks hold the phone numbers, SIP credentials, and provider settings and can be used by multiple agents. This lets you centrally manage numbers and reuse them across agents or workflows. If you need different caller IDs per use case, create separate outbound trunks (e.g., sales vs support) or specify a
sip_number
when creating a SIP participant — note this depends on your SIP provider allowing caller ID override. Would you like an example of creating a SIP participant with a
sip_trunk_id
or help designing trunk/number mappings for your agents? Sources: Agents telephony integration | LiveKit docs | sip | livekit/livekit_composite | DeepWiki | Outbound Calling Script | LiveKit docs | Preserving Caller ID During SIP Transfers in LiveKit Agents
l
so how does the mapping happen say i cal number A and i want agent B to handle it
t
You're asking how an incoming call to phone number A gets routed so agent B handles it. Routing is done at the project level using inbound trunks + dispatch rules (or phone-number dispatch). Key points: • Inbound trunks receive calls for one or more phone numbers. They do not map directly to agents. • Dispatch rules tied to an inbound trunk determine where a call goes. A dispatch rule can: ◦ Match by called number or number prefix. ◦ Specify a target room pattern (e.g., rooms named
call-...
) or directly target an agent by
agentName
in
roomConfig.agents
. • The dispatch rule creates/chooses a room and then the roomConfig routes the call to the specified agent(s). Example flow (simple): 1. Call to number A arrives on inbound trunk X. 2. Dispatch rule for number A matches and targets room prefix
call-A-
or agent
agentB
. 3. LiveKit creates a room (e.g.,
call-A-123
) and applies
roomConfig
that lists
agentB
. 4. Agent B is dispatched into that room to handle the call. Options you can use: • Direct agent mapping: set the dispatch rule to target
agentName: "agentB"
. • Room-based mapping: dispatch to a room prefix and use roomConfig to include specific agents or agent groups. • Use multiple dispatch rules to route different numbers/conditions to different agents or queues. If you want, I can: • Provide an example dispatch rule JSON that routes number A to agent B. • Suggest a pattern for room naming and agent pools if you need load-balancing or skill-based routing. Which would you prefer?
l
i meant an outbound call ?
t
Thanks — you're asking how to route an outbound call so that a specific agent (B) makes the call to number A. For outbound calls the mapping is done at call creation time by the agent/service that initiates the call. Key points: • Outbound calls are placed through an organization-level outbound trunk. When you create the outbound SIP participant you specify which trunk to use (the
sip_trunk_id
) and optionally which number to present as caller ID (
sip_number
). • The agent who should make the call is the one that initiates the CreateSIPParticipant request (or your service creates the participant on behalf of that agent). That participant is tied to a room or agent identity so the call is associated with agent B. • Practically you have two common patterns: ◦ Agent-initiated: Agent B triggers the call from their client; the client/server calls CreateSIPParticipant with
sip_trunk_id
(and optional
sip_number
) and the participant is created for Agent B’s room/identity — Agent B is the caller. ◦ Server-initiated on behalf of agent: Your server issues CreateSIPParticipant specifying the target room/agent identity (Agent B) so LiveKit creates the SIP leg and maps it into Agent B’s room; Agent B is then connected to the outbound call. If you want, I can provide a sample CreateSIPParticipant request showing the fields to set (
sip_trunk_id
,
sip_number
, and the room/participant identity) for Agent B. Which pattern are you using: agent-initiated or server-initiated?
l
in my architecture i assign eachh agent a unique phone number for inbound calls thus if i set the phone number while making outboudn calls . it would know which agent is calling right ?
t
You're on the right track — setting the phone number when making an outbound call can indicate which agent is calling, but there are important details: • If you set the agent's unique phone number in the outbound request (e.g., via
sip_number
on
CreateSIPParticipant
), the call will present that number as the caller ID so recipients see the agent's number. • LiveKit does not automatically infer agent identity just from caller ID. The agent association comes from who the participant/room/identity is for when you create the SIP participant. So you must: ◦ Create the SIP participant for Agent B's room/identity, and ◦ Specify the agent's phone number as the
sip_number
(or use the outbound trunk whose number matches the agent). • Caller ID override depends on your SIP provider allowing
sip_number
passthrough. If the provider blocks overrides, use a dedicated outbound trunk whose number equals the agent's number. If you'd like, I can: • Provide a sample
CreateSIPParticipant
request that ties the outbound call to Agent B and sets
sip_number
, or • Recommend trunk/number mapping patterns (one trunk per agent number vs shared trunks with
sip_number
override). Which do you prefer?
l
i wrote my create participant in this manner. i have a common outbound trunk at org level async initiateCall( // agentId: string, trunkId: string, fromNumber: string, toNumber: string, options?: { roomName?: string; displayName?: string; dtmf?: string; playDialtone?: boolean; waitUntilAnswered?: boolean; krispEnabled?: boolean; participantMetadata?: any; } ) { try { console.log(
Initiating call from ${fromNumber} to ${toNumber}
); // // Validate agent // const agent = await prisma.agents.findUnique({ // where: { id: agentId }, // }); // if (!agent) { // throw new Error('Agent not found'); // } // if (agent.status !== 'ACTIVE') { // throw new Error('Agent is not active'); // } // // Validate trunk // const trunk = await prisma.sipOutboundTrunk.findUnique({ // where: { id: trunkId }, // }); // if (!trunk) { // throw new Error('Trunk not found'); // } // if (!trunk.numbers.includes(fromNumber)) { // throw new Error(
Number ${fromNumber} not configured on trunk
); // } // Generate room name if not provided const callRoomName = options?.roomName || `outbound-call-${Date.now()}`; // const sipParticipantOptions = { // participantIdentity: 'sip-test', // participantName: 'Test Caller', // krispEnabled: true, // waitUntilAnswered: true // }; // Create outbound call record in database // const outboundCall = await prisma.outboundCall.create({ // data: { // agent_id: agentId, // trunk_id: trunkId, // fromNumber, // toNumber, // roomName: callRoomName, // status: 'INITIATED', // }, // }); // console.log(
:white_check_mark: Outbound call record created: ${outboundCall.id}
); // Prepare participant metadata // const participantMetadata = { // // call_id: outboundCall.id, // agent_id: agentId, // from_number: fromNumber, // to_number: toNumber, // ...(options?.participantMetadata || {}), // }; // Create SIP participant with full options // const participantOptions = { // // participant_identity:
outbound-${outboundCall.id}
, // participant_name: options?.displayName ||
Outbound Call to ${toNumber}
, // participant_metadata: JSON.stringify(participantMetadata), // display_name: options?.displayName, // Custom caller ID // dtmf: options?.dtmf, // Extension codes (e.g., "*123#ww456") // play_dialtone: options?.playDialtone ?? false, // Play dial tone while connecting // wait_until_answered: options?.waitUntilAnswered ?? true, // Wait for answer // krisp_enabled: options?.krispEnabled ?? true, // Noise cancellation // }; // console.log('Creating SIP participant with options:', participantOptions); const participantInfo = await this.sipClient.createSipParticipant( trunkId, toNumber, callRoomName, { waitUntilAnswered: true, krispEnabled: true } ); console.log(
SIP participant created: ${participantInfo.participantId}
); // Update call with participant ID and status // const updatedCall = await prisma.outboundCall.update({ // where: { id: outboundCall.id }, // data: { // participantId: participantInfo.participantId, // status: participantInfo.participantId ? 'IN_PROGRESS' : 'RINGING', // }, // }); return { success: true, // call: updatedCall, participantInfo, roomName: callRoomName, message: options?.waitUntilAnswered ? 'Call answered and connected' : 'Call initiated', }; } catch (error: any) { console.error('Failed to initiate call:', error); if (error.metadata) { const sipStatusCode = error.metadata['sip_status_code']; const sipStatus = error.metadata['sip_status']; console.error(
SIP Error: ${sipStatusCode} - ${sipStatus}
); throw new Error(
SIP Error: ${sipStatusCode} - ${sipStatus || error.message}
); } throw error; } }
t
Thanks — you're asking whether your current implementation will ensure Agent B is associated with the outbound call when you set
fromNumber
. Short answer: not automatically. Setting
fromNumber
as the caller ID helps recipients see the agent's number, but LiveKit associates the call with an agent by the participant identity/room you create — not by caller ID alone. To guarantee Agent B handles the call, create the SIP participant for Agent B’s room/identity (or include Agent B’s identity in the participant metadata) when calling
createSipParticipant
. What to change in your code (concise checklist): • Ensure the participant identity is tied to the agent (e.g.,
participantIdentity: agent-${agentId}
) or create the participant in Agent B’s room. • Pass
sip_number
(or equivalent field your SDK uses) with
fromNumber
so caller ID displays correctly — only works if your SIP provider allows override. • Keep
sip_trunk_id
as the org-level trunk. • Include agent info in
participant_metadata
(e.g.,
agent_id
,
from_number
) so you can reconcile records and routing on events/callbacks. • If provider blocks caller ID override, use a trunk whose number matches the agent’s number. If you want, I can: • Provide a short sample
createSipParticipant
payload showing
participantIdentity
,
participantMetadata
,
sip_number
, and
sip_trunk_id
. • Review a patched version of your function with those fields inserted. Which would you prefer?
l
while creating agent i dont assign rooms to it . instead i let livekit handle it dynamically
t
Thanks — you mentioned you let LiveKit create rooms dynamically. That’s fine; just ensure the SIP participant you create is tied to the agent’s identity/room so the outbound call is associated with Agent B. Concrete changes to make in your flow (short, actionable): • Create the SIP participant for the agent’s identity or agent-specific room ◦ Use a participant identity that encodes the agent, e.g.
participantIdentity: agent-${agentId}
or place the participant into a room you treat as Agent B’s room (even if created dynamically). • Include agent info in participant metadata ◦ Pass
participant_metadata: JSON.stringify({ agent_id: agentId, from_number: fromNumber, ... })
so events/webhooks can reconcile calls to agents. • Set caller ID explicitly ◦ Provide
sip_number: fromNumber
(or SDK equivalent) when creating the SIP participant so the recipient sees the agent’s number. This requires your SIP provider to allow caller ID override; otherwise use a trunk with that number. • Keep using the org-level trunk ◦ Continue passing
sip_trunk_id
(your trunkId). • Ensure your server or client is the one that initiates the CreateSipParticipant call on behalf of the agent ◦ Either the agent’s client calls the API, or your server calls it specifying the agent identity so LiveKit maps the SIP leg to that agent. If you want, I can produce a minimal example payload for your
createSipParticipant
call (showing
sip_trunk_id
,
sip_number
,
participant_identity
, and
participant_metadata
) or patch your function to include these fields. Which would you prefer?
l
* Create a new SIP participant. * * @average-fall-32270 sipTrunkId - sip trunk to use for the call * @average-fall-32270 number - number to dial * @average-fall-32270 roomName - room to attach the call to * @average-fall-32270 opts - CreateSipParticipantOptions * @average-fall-32270 outboundTrunkConfig - Optional outbound trunk configuration for sip participant. * @returns Created SIP participant async createSipParticipant( sipTrunkId: string, number: string, roomName: string, opts?: CreateSipParticipantOptions, outboundTrunkConfig?: SIPOutboundConfig, ): Promise<SIPParticipantInfo> { if (opts === undefined) { opts = {}; } if (opts.timeout === undefined) { opts.timeout = opts.waitUntilAnswered ? 60 : 10; } const req = new CreateSIPParticipantRequest({ sipTrunkId: sipTrunkId, trunk: outboundTrunkConfig, sipCallTo: number, sipNumber: opts.fromNumber, roomName: roomName, participantIdentity: opts.participantIdentity || 'sip-participant', participantName: opts.participantName, displayName: opts.displayName, participantMetadata: opts.participantMetadata, participantAttributes: opts.participantAttributes, dtmf: opts.dtmf, playDialtone: opts.playDialtone ?? opts.playRingtone, headers: opts.headers, hidePhoneNumber: opts.hidePhoneNumber, includeHeaders: opts.includeHeaders, ringingTimeout: opts.ringingTimeout ? new Duration({ seconds: BigInt(opts.ringingTimeout) }) : undefined, maxCallDuration: opts.maxCallDuration ? new Duration({ seconds: BigInt(opts.maxCallDuration) }) : undefined, krispEnabled: opts.krispEnabled, waitUntilAnswered: opts.waitUntilAnswered, }).toJson(); const data = await this.rpc.request( svc, 'CreateSIPParticipant', req, await this.authHeader({}, { call: true }), opts.timeout, ); return SIPParticipantInfo.fromJson(data, { ignoreUnknownFields: true }); } what does this fucntion params do explain
t
You're asking what each parameter and option in your
createSipParticipant
function does. Below is a concise explanation tied to how the call is created and mapped into a LiveKit room. Function purpose • Initiates an outbound SIP call and bridges the phone leg into a LiveKit room as a SIP participant. Top-level parameters • sipTrunkId (string): ID of the outbound SIP trunk to use for the call. The trunk contains provider/auth details and determines how the call is sent. • number (string): Destination phone number to dial (the call target, sent as
sipCallTo
). • roomName (string): LiveKit room the SIP participant will join when the call connects. • opts (CreateSipParticipantOptions, optional): Call-level options that control caller ID, identity, behavior, and features (see fields below). • outboundTrunkConfig (SIPOutboundConfig, optional): Inline trunk configuration to use instead of referencing a pre-configured trunk (overrides
sipTrunkId
behavior). Key fields in the constructed request and what they do • sipTrunkId / trunk: selects the trunk used to place the outbound INVITE (organization-level trunk or inline config). • sipCallTo: the number being dialed (destination). • sipNumber (
opts.fromNumber
): caller ID presented to the callee. Only effective if the SIP provider allows caller ID override; otherwise use a trunk whose number matches the desired caller ID. • roomName: name of the room the SIP participant will join (the phone leg is bridged into this room). • participantIdentity: unique identity for the SIP participant in the room (use something encoding the agent, e.g.,
agent-{agentId}
to tie the call to a specific agent). • participantName / displayName: human-readable participant name shown in UIs. • participantMetadata: arbitrary JSON metadata attached to the participant (use to store agent_id, from_number, call_id, CRM links, etc.). • participantAttributes: key/value attributes published to the room (useful for client-side logic and event handling). • dtmf: DTMF digits to send after connect (for IVR navigation, PIN entry). • playDialtone / playRingtone: play local dial tone/ringtone in the room while dialing so agents hear dialing state. • headers: custom SIP headers to include on the INVITE (for integrations needing SIP header data). • includeHeaders: include inbound SIP headers from provider in participant attributes (useful for debugging or extracting SIP metadata). • hidePhoneNumber: mask phone number in identities/logs for privacy. • ringingTimeout: seconds to wait for ringing/answer before canceling or treating as failed. • maxCallDuration: maximum call length in seconds (server-enforced cut-off). • krispEnabled: enable Krisp noise cancellation on the phone audio. • waitUntilAnswered: whether the API call should wait until the callee answers before returning; affects default timeout. • timeout: overall RPC timeout for the request (defaults to 10s, or 60s when waitUntilAnswered is true). Return • Promise<SIPParticipantInfo>: contains participant ID, room, SIP call id, and current state (dialing/connected/ended) so you can track/monitor the call. How this ties to agent mapping (practical notes) • Caller ID alone does not bind the call to an agent. To ensure Agent B handles the outbound call: ◦ Create the SIP participant with a participantIdentity that encodes Agent B or place the participant into a room that Agent B will join. ◦ Include agent info in participantMetadata so webhooks/events can reconcile the participant to agent records. ◦ Set sipNumber if you want the callee to see Agent B’s phone number and your SIP provider allows overrides; otherwise use a trunk whose number equals the agent number. If you want, I can: • Provide a minimal JSON payload example for
CreateSIPParticipantRequest
showing the recommended fields to tie the call to an agent, or • Patch your function to populate
participantIdentity
,
participantMetadata
, and
sipNumber
. Which would you like? Sources: Outbound Calling Script | LiveKit docs | Agents telephony integration | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docs | Preserving Caller ID During SIP Transfers in LiveKit Agents | sip | livekit/livekit_composite | DeepWiki
l
basically the participantName / displayName: human-readable participant name shown in UIs is of the human we are callinf right ?
t
Short answer: usually yes —
participantName
/
displayName
is the human-readable label shown in UIs and is typically set to represent the person you are calling (the callee). Clarifications and best-practices: • It can be any descriptive string — caller/callee name, “Customer”, CRM name, or phone number — whatever your UI/UX needs. • Use
participantMetadata
to store authoritative data (e.g.,
customer_id
, phone, CRM link). That lets you show a friendly
displayName
while reliably reconciling events to records. • If you want the agent identity to be obvious in the room, encode the agent in
participantIdentity
(e.g.,
agent-{agentId}
) and keep
displayName
for the remote person. • For privacy, avoid putting sensitive PII in `displayName`; use
hidePhoneNumber
or mask in UI if needed. Do you want a suggested
displayName
/
participantMetadata
schema for your app?
l
is dispatch rule only for inbound calls or outbound too
t
Dispatch rules are for inbound call routing only. • Inbound: Dispatch rules (or phone-number dispatch) determine how calls arriving on an inbound trunk or LiveKit phone number are routed to rooms/agents (matching by called number, prefix, or directly specifying
agentName
/
roomConfig.agents
). • Outbound: Outbound calls are initiated by creating a SIP participant and selecting an outbound trunk (and optionally
sip_number
). Routing for outbound is determined at call creation (which trunk, which room/participant identity) — dispatch rules do not affect outbound calls. If you want, I can show a sample dispatch rule for inbound routing or a sample CreateSIPParticipant payload that ties an outbound call to a specific agent. Which would you prefer?
l
create a sample CreateSIPParticipant payload that ties an outbound call to a specific agent.
t
Below is a minimal CreateSIPParticipant payload that ties an outbound call to a specific agent, plus short notes on the key fields. JSON payload (body sent to CreateSIPParticipant):
Copy code
{
  "sipTrunkId": "ST_abc123",            // org-level trunk ID
  "trunk": null,                        // optional inline trunk config (leave null if using sipTrunkId)
  "sipCallTo": "+14155551234",         // destination (callee)
  "sipNumber": "+15551230000",         // caller ID to present (agent's number)
  "roomName": "agent-room-42",         // room to bridge the phone leg into
  "participantIdentity": "agent-42",   // ties the SIP participant to Agent B
  "participantName": "Acme Corp Caller",
  "displayName": "Jane Customer",
  "participantMetadata": "{\"agent_id\":\"42\",\"agent_name\":\"Agent B\",\"call_id\":\"out-9876\"}",
  "participantAttributes": {
    "agent_id": "42",
    "from_number": "+15551230000",
    "to_number": "+14155551234",
    "call_type": "outbound"
  },
  "dtmf": null,
  "playDialtone": true,
  "krispEnabled": true,
  "waitUntilAnswered": true,
  "ringingTimeout": 60,
  "maxCallDuration": 3600,
  "headers": {
    "X-Custom-Header": "some-value"
  },
  "hidePhoneNumber": false,
  "includeHeaders": true
}
Key notes • Use
participantIdentity
(e.g.,
agent-42
) or the
roomName
that Agent B will join so LiveKit associates the SIP leg with that agent. •
sipTrunkId
selects the org trunk;
sipNumber
sets caller ID (only works if your SIP provider allows override). If provider blocks overrides, use a trunk whose number equals the agent number. • Put stable identifiers in
participantMetadata
(agent_id, call_id, CRM id) so webhooks/events can reconcile the participant to your agent records. •
participantAttributes
are visible in the room for client logic and can be used to drive UI/state. •
waitUntilAnswered: true
makes the API call wait until the callee answers before returning — useful so the agent only joins after connection. • Adjust
krispEnabled
, timeouts, and DTMF as needed. Would you like this as a TypeScript object or a patched version of your function that injects these fields?
l
would it be bad to have a new outboudn trunk for every new voice agent
t
Short answer: it’s not recommended to create a new outbound trunk for every agent. Why: • Outbound trunks are infrastructure resources (provider credentials, endpoints, routing). Managing one per agent adds large operational overhead (credential rotation, provider limits, billing) and scales poorly. • LiveKit is designed for shared trunks: multiple agents can place calls through the same org-level trunk by specifying the
sip_trunk_id
when creating a SIP participant. • Caller identity should be handled at call creation: set
sipNumber
(caller ID) and `participantIdentity`/`participantMetadata` to tie the call to an agent. If your SIP provider blocks caller ID overrides, use trunks mapped to the desired number(s), but still prefer a smaller set of trunks (per region/use-case), not per agent. When separate trunks make sense: • Regulatory/geographic requirements (different providers or number pools per region) • Distinct business lines requiring separate providers or billing • Test/staging isolation Recommendation: • Use shared outbound trunks (one or a few by region/use-case). • Associate calls to agents via
participantIdentity
,
participantMetadata
, room assignment, and
sipNumber
when allowed. • Create additional trunks only for regulatory, regional, or provider-specific needs — not per-agent. Would you like a suggested mapping pattern (e.g., one trunk per region + caller ID override strategy) for your setup? Sources: sip | livekit/livekit_composite | DeepWiki | Making calls overview | LiveKit docs | Outbound Calling Script | LiveKit docs | examples/voice_agents | livekit/livekit_composite | DeepWiki | Python SDKs | livekit/livekit_composite | DeepWiki
l
provide a sample response of the
ListSIPOutboundTrunk
API in node js
t
I don't have the answer you're looking for. You could also try asking your question: • in one of the other Slack channels or • to https://deepwiki.com/livekit/livekit_composite which is trained on all LiveKit source code If you find the answer, please post it here to help others!
l
will the createSipOutboundTrunk work if i dont pass any phone numbr initially