Inbound Calling System
End-to-end documentation for inbound call handling in Oktivo.
Architecture Overview
Important (2026-07 Update): The inbound system now uses a per-Provider model instead of per-number trunks/rules. A single shared LiveKit inbound trunk and dispatch rule serves ALL phone numbers for a given provider within a deployment environment. Agent identity is resolved at runtime from the dialed number via the Inbound Resolution API, not from static dispatch metadata.
Key Concepts: Per-Provider Model
Why Per-Provider Instead of Per-Number?
The previous per-number model created a separate LiveKit inbound trunk and dispatch rule for each phone number. This caused issues:
- Multi-developer conflicts: When developers share a LiveKit server, each developer's backend would create its own trunk for the same number, causing routing conflicts (486 "flood" errors)
- Scalability: Hundreds of phone numbers meant hundreds of LiveKit resources to manage
- Stale metadata: Static
agent_idin dispatch rules became stale when agents were reassigned
The new per-Provider model solves these by:
- One trunk per provider per environment:
{provider}-inbound-{deploymentEnv}(e.g.,voicelink-inbound-production) - One dispatch rule per provider: Shared across all numbers, with NO per-number metadata
- Runtime resolution: Agent identity resolved from dialed number via API call, not static metadata
Environment Isolation: deploymentEnv
The deploymentEnv config value (from DEPLOYMENT_ENV env var or derived from AGENT_NAME) ensures:
- Multiple developers sharing a LiveKit server don't conflict
- Production vs staging environments have separate resources
- Each developer's local environment is isolated
# Developer Alice
DEPLOYMENT_ENV=dev-alice # or AGENT_NAME=Oktivo-Dev-Alice
# Developer Bob
DEPLOYMENT_ENV=dev-bob # or AGENT_NAME=Oktivo-Dev-Bob
# Production
DEPLOYMENT_ENV=production # auto-derived when NODE_ENV=productionDeterministic Resource Naming
LiveKit resources use deterministic names that any process can compute:
| Resource | Naming Convention | Example |
|---|---|---|
| Inbound Trunk | {provider}-inbound-{deploymentEnv} | voicelink-inbound-production |
| Dispatch Rule Room Prefix | inbound-{provider}-{deploymentEnv}- | inbound-voicelink-production- |
| Room Name | {roomPrefix}{random} | inbound-voicelink-production-a1b2c3d4 |
This allows any backend process to identify "is this trunk mine?" without querying its local MongoDB.
Setup Flow
Step 1: Superadmin Creates Inbound Trunk
UI: TrunkManagementPage.tsx → Create Trunk dialog with direction: "inbound" or "both"
API: POST /api/telephony/trunks
What happens:
telephonyService.createTrunk()creates an outbound trunk on LiveKit- If direction is
"inbound"or"both":- Calls
getOrCreateProviderInboundTrunk()looks up existing trunk by deterministic name{provider}-inbound-{deploymentEnv} - If found, reconciles numbers to include all provider numbers
- If not found, creates new trunk with deterministic name
- Calls
- Stores
livekitTrunkId,livekitInboundTrunkId,providerKey, anddeploymentEnvin theSIPTrunkMongoDB document
Files: telephony.service.ts → createTrunk(), livekit.service.ts → getOrCreateProviderInboundTrunk()
Step 2: Superadmin Adds Numbers to Pool
API: POST /api/telephony/numbers
Numbers are added to the pool linked to the trunk via trunkUID. Direction is not set on the number itself it's configured per-assignment.
Step 3: Superadmin Assigns Number to Org
API: POST /api/telephony/numbers/:id/assign-org
Creates a PhoneNumberAssignment document with orgUID, direction (outbound/inbound/both), and customerUID: null (org-level).
Step 4: Admin Assigns Number to Customer
API: POST /api/telephony/numbers/:id/assign-customer
Creates a PhoneNumberAssignment document with customerUID set. Direction must be compatible with the org-level assignment.
Step 5: Customer Links Number to Agent (Activation)
UI: AgentEditorPage.tsx → Inbound Number section
API: POST /api/customer/phone-numbers/:assignmentUID/activate-inbound
What happens (telephonyService.activateInbound()):
- Atomic uniqueness guard FIRST:
Agent.findOneAndUpdatewith conditioninboundPhoneUID: null OR this phoneUIDensures only one agent can own a phone number. If this fails, NO further mutation happens (Requirement 6.3) - Validates assignment direction supports inbound (not "outbound"-only)
- Computes union of all active inbound numbers for this provider via
getProviderInboundNumbers() - Calls
getOrCreateProviderInboundTrunk()reuses existing shared trunk or creates it - Calls
getOrCreateProviderDispatchRule()reuses existing shared dispatch rule or creates it with minimal metadata:jsonNote: NO{ "mode": "inbound", "need_to_update_agent": true }agent_id,phone_uid,orgUID, orcustomerUIDin dispatch metadata these are resolved at runtime - Updates
PhoneNumberAssignment.inboundConfigwithisActive: true,dispatchRuleId,defaultAgentId - Updates
SIPTrunkwithlivekitInboundTrunkId,providerKey,deploymentEnv
Files: telephony.service.ts → activateInbound(), livekit.service.ts → getOrCreateProviderInboundTrunk(), getOrCreateProviderDispatchRule()
Inbound Call Flow (Runtime)
1. SIP Call Arrives
LiveKit SIP server receives the INVITE, matches the shared provider inbound trunk by phone number, finds the shared dispatch rule by trunkIds, creates a room with the provider-scoped prefix (inbound-{provider}-{env}-{random}), and places the SIP caller as a participant.
2. Webhook: Contact Creation (Runtime Resolution)
The participant_joined webhook fires. The webhook handler in webhook.controller.ts detects the inbound- room prefix and:
- Reads the dialed number from SIP attributes NOT from the room name:javascript
const dialedNumber = sipAttrs["sip.trunkPhoneNumber"] || sipAttrs["sip.phoneNumber"]; - Resolves the inbound assignment via
phoneNumberAssignmentRepository.resolveInboundNumber(dialedNumber) - If no active assignment found, logs a warning and skips Contact creation
- Checks org concurrency limits
- Creates a Contact document on-the-fly with:
contactUID: random nanoid (NOT the SIP identity)roomId: the LiveKit room namecallDirection:"inbound"campaignUID:inbound_{assignmentUID}_{YYYYMMDD}AgentID,orgUID,customerUID: from the resolved assignments3RecordingKey: pre-built S3 path for recording
3. Agent Dispatch
LiveKit dispatches the job to the agent worker matching agentName. The agent receives ctx.job.metadata containing minimal dispatch rule metadata:
{ "mode": "inbound", "need_to_update_agent": true }Note: There is NO agent_id, phone_uid, orgUID, or customerUID in the metadata these are resolved at runtime by the Agent Server.
4. Agent Entrypoint (agent.py)
resolve_metadata()parsesctx.job.metadata→ detectsmode: "inbound"- Skips
sync_agent_settings()for inbound mode agent identity isn't known yet - Creates a placeholder
AgentMetaSchema()for session bootstrap - Enters the
inboundmode handler → createsInboundCallHandler
5. InboundCallHandler (inbound_call_handler.py)
- Find SIP participant: Polls room for non-agent SIP participant (kind=3)
- Extract caller phone: From
sip.phoneNumberattribute - Extract dialed number: From
sip.trunkPhoneNumberattribute (CRITICAL) - Resolve agent identity via API (NEW):python
resolution = await self._resolve_inbound_number(dialed_number) # Returns: { agentId, orgUID, customerUID, assignmentUID, greeting } - Update metadata with resolved values:python
self.meta.agent_id = resolution["agentId"] self.meta.organization_id = resolution.get("orgUID") self.meta.customer_id = resolution.get("customerUID") - Fetch and apply agent config:python
agent_config = await self._fetch_target_agent_config(resolution["agentId"]) self.agent_meta = AgentMetaSchema(**agent_config) self.session.update_agent(resolved_voice_agent) # Hot-swap the agent - Compute campaign_id:
inbound_{assignmentUID}_{YYYYMMDD}(usesassignmentUIDfrom resolution) - Resolve contactUID: Calls
POST /resolve-inbound-contactto get the real contactUID - Start recording: Calls
POST /start-recording - Say first message: Uses resolved
agent_meta.first_message - Store metadata: Updates
ctx.userdatafor post-call sync
6. Post-Call
On disconnect, the agent's shutdown_callback runs post_call_event() which:
- Syncs transcription to Contact via
POST /transcription-sync - Triggers analysis via Analysis service
- Stores observability metrics via
POST /contact-observability
Recording Flow for Inbound
Key difference from outbound: The /start-recording endpoint now falls back to roomId lookup when contactUID doesn't match (because inbound Contacts use a random nanoid, not the SIP identity).
Inbound Resolution API
The Agent Server resolves agent identity at runtime via this internal endpoint:
POST /internal/resolve-inbound-number
Request:
{
"dialedNumber": "+919876543210"
}Headers:
x-api-secret: <WEBHOOK_SECRET>Success Response (200):
{
"success": true,
"agentId": "r7_vZ96yewMc",
"orgUID": "org_JAP4GGXe3qYAwJlF",
"customerUID": "usr_wDhBnR9HnXuUzKu0",
"assignmentUID": "asgn_y0fQQAr6Gpm4WdDr",
"greeting": "Hello, how can I help you today?"
}Not Found Response (404) returned for:
- Number not in system
inboundConfig.isActive: false- No
defaultAgentIdconfigured
{
"success": false,
"message": "No active inbound configuration for this number"
}Implementation: webhook.controller.ts → resolveInboundNumber()
Schema Changes
SIPTrunk Model
interface ISIPTrunk {
// ... existing fields ...
livekitInboundTrunkId: string | null; // LiveKit inbound trunk ID (null for outbound-only)
// NEW: Per-provider model fields
deploymentEnv: string; // Deployment environment (e.g., "production", "dev-alice")
providerKey: string | null; // "{provider}:{deploymentEnv}" unique sparse index
}The providerKey field ensures at most one inbound-capable SIPTrunk doc per provider per deployment environment within a single MongoDB. The sparse index allows outbound-only trunks to have providerKey: null.
PhoneNumber Model (Pool Entity)
phoneUID: String (unique) // Auto-generated identifier
number: String (unique) // The phone number
trunkUID: String // Which SIP trunk routes this number
status: "available" | "suspended"
provider: String // SIP provider name
label: String | null // Optional display labelNote:
inboundConfigno longer lives on PhoneNumber. It's onPhoneNumberAssignment.
PhoneNumberAssignment Model
assignmentUID: String (unique) // Auto-generated identifier
phoneUID: String // References PhoneNumber
number: String // Denormalized for quick lookups
orgUID: String // Organization
customerUID: String | null // null = org-level assignment
direction: "outbound" | "inbound" | "both"
status: "active" | "suspended"
inboundConfig: {
dispatchRuleId: String | null, // Shared LiveKit dispatch rule ID
defaultAgentId: String | null, // Agent linked for inbound
greeting: String | null, // Custom greeting message
isActive: Boolean // Whether inbound routing is live
}
assignedAt: DateAgent Model
inboundPhoneUID: String | null // Phone number linked for inbound (unique sparse index)Contact Model
callDirection: "outbound" | "inbound" // Distinguishes call typeFile Changes Summary
Agent Server (Python)
| File | Change | Why |
|---|---|---|
agent.py | Skips sync_agent_settings() for mode == "inbound" | Agent identity unknown until runtime resolution; uses placeholder AgentMetaSchema() |
agent.py | Added inbound mode handler | Routes to InboundCallHandler |
src/handlers/inbound_call_handler.py | Added _resolve_inbound_number() | Calls /internal/resolve-inbound-number API with bounded retries |
src/handlers/inbound_call_handler.py | Updated start_call() | Reads sip.trunkPhoneNumber, resolves agent via API, hot-swaps session agent |
src/schemas/metadata_schema.py | extra = 'allow' already set | Accepts dispatch metadata fields |
requirements-dev.txt | Added hypothesis>=6.100.0 | Property-based testing support |
Client Backend (Node.js)
| File | Change | Why |
|---|---|---|
config/index.ts | Added deploymentEnv config | Distinguishes resources across environments |
models/SIPTrunk.ts | Added deploymentEnv, providerKey fields | Per-provider model support |
services/livekit.service.ts | Added inboundTrunkName(), providerRoomPrefix() | Deterministic naming helpers |
services/livekit.service.ts | Added getOrCreateProviderInboundTrunk() | Idempotent lookup-by-name-then-create |
services/livekit.service.ts | Added getOrCreateProviderDispatchRule() | Shared dispatch rule with NO per-number metadata |
services/livekit.service.ts | Added migrateDispatchRulesToTrunk(), selectDuplicateTrunkSurvivor() | Duplicate-collapse helpers for trunk sync |
services/livekit.service.ts | Reworked cleanupStaleInboundTrunks() | Name-driven (not DB-driven) stale trunk cleanup |
services/telephony.service.ts | Added getProviderInboundNumbers() | Computes union of numbers across all customers for a provider |
services/telephony.service.ts | Reworked activateInbound() | Atomic uniqueness guard FIRST, then shared trunk/rule reuse |
services/telephony.service.ts | Reworked updateInboundAgent() | Same atomic guard, reuses existing dispatch rule |
services/telephony.service.ts | Reworked deactivateInbound() | Clears agent link, does NOT delete shared trunk/rule |
services/telephony.service.ts | Removed ensureInboundTrunkExists() | Replaced by getOrCreateProviderInboundTrunk() |
services/trunkSync.service.ts | Added getActiveInboundProviders() | Lists providers with ≥1 active inbound number |
services/trunkSync.service.ts | Reworked syncInboundTrunks() | Per-provider reconciliation with duplicate collapse |
services/trunkSync.service.ts | Reworked syncDispatchRules() | Per-provider reconciliation, writes dispatchRuleId to all active assignments |
services/trunkSync.service.ts | Added duplicatesRemoved, providersReconciled to SyncReport | New sync metrics |
controllers/webhook.controller.ts | Updated participant_joined for inbound | Reads sip.trunkPhoneNumber from SIP attributes, resolves via resolveInboundNumber() |
controllers/webhook.controller.ts | Added resolveInboundNumber controller | /internal/resolve-inbound-number endpoint |
routes/webhook.routes.ts | Added /internal/resolve-inbound-number route | New internal API endpoint |
scripts/migrate-inbound-trunks.ts | One-off migration (since removed) | Migrated legacy per-number trunks; the current trunk migration script is scripts/migrate-sip-trunks.ts |
Dispatch Rule Metadata
Important Change (2026-07): The dispatch rule metadata is now minimal and does NOT contain per-number agent identity. Agent identity is resolved at runtime.
Old Model (Deprecated)
{
"mode": "inbound",
"agent_id": "r7_vZ96yewMc",
"orgUID": "org_JAP4GGXe3qYAwJlF",
"customerUID": "usr_wDhBnR9HnXuUzKu0",
"sip_phone_number": "+918071387312",
"phone_uid": "phn_y0fQQAr6Gpm4WdDr",
"need_to_update_agent": true
}New Model (Per-Provider)
{
"mode": "inbound",
"need_to_update_agent": true
}Why the change? The dispatch rule is now shared across ALL phone numbers for a provider. Baking a single number's agent_id/orgUID/customerUID into the metadata would misroute every other number that shares the rule.
Instead, the Agent Server:
- Reads
sip.trunkPhoneNumberfrom the SIP participant's attributes - Calls
POST /internal/resolve-inbound-numberto get the real agent identity - Fetches the resolved agent's configuration
- Hot-swaps the session agent with the resolved config
Inbound vs Outbound: Key Differences
| Aspect | Outbound | Inbound |
|---|---|---|
| Who initiates | Agent calls create_sip_participant() | Caller dials the number, LiveKit creates room |
| SIP participant | Created by agent | Already in room when agent joins |
| Contact creation | Before call starts (campaign flow) | On-the-fly by webhook handler |
| contactUID source | Set during campaign dispatch | Random nanoid from webhook |
| Agent identity | Known at dispatch time | Resolved at runtime via API |
| Recording trigger | Agent calls /start-recording after SIP answers | Agent calls /start-recording after joining room |
| First message | agent_meta.first_message | agent_meta.first_message (resolved agent) |
| Campaign ID | From campaign document | inbound_{assignmentUID}_{YYYYMMDD} |
| Room name | room-{uuid} | inbound-{provider}-{env}-{random} |
| Trunk model | Per-trunk (outbound only) | Per-provider (shared across all numbers) |
| Dispatch rule | Per-campaign | Shared per-provider (no agent identity in metadata) |
Multi-Developer Environment
When multiple developers share the same LiveKit server:
Each developer MUST use a unique
DEPLOYMENT_ENV(orAGENT_NAMEas fallback) in.env:bashDEPLOYMENT_ENV=dev-alice # Recommended # or AGENT_NAME=Oktivo-Dev-Alice # Used as fallback if DEPLOYMENT_ENV not setThe per-provider model creates environment-isolated resources:
- Trunk name:
{provider}-inbound-{deploymentEnv}(e.g.,voicelink-inbound-dev-alice) - Room prefix:
inbound-{provider}-{deploymentEnv}-(e.g.,inbound-voicelink-dev-alice-)
- Trunk name:
Stale trunk cleanup is name-driven: When
getOrCreateProviderInboundTrunk()creates a trunk, it cleans up any other trunks that:- Have overlapping phone numbers, AND
- Do NOT have the expected deterministic name for this
(provider, deploymentEnv)
This prevents conflicts without requiring database coordination between developers.
Trunk sync auto-heals: On startup and every minute,
trunkSync.service.ts:- Reconciles provider trunks (create missing, collapse duplicates)
- Reconciles dispatch rules (create missing, remove duplicates)
- Updates all
PhoneNumberAssignmentdocs with the correctdispatchRuleId
Environment Variable Reference
| Variable | Description | Example |
|---|---|---|
DEPLOYMENT_ENV | Primary deployment environment identifier | production, dev-alice, staging |
AGENT_NAME | LiveKit agent name (also used as deploymentEnv fallback) | Oktivo-Dev-Alice |
NODE_ENV | If production, deploymentEnv defaults to "production" | production, development |
Common Multi-Developer Pitfall: "flood" / 486 Errors
When multiple developers create inbound trunks for the same phone number on a shared LiveKit server WITHOUT using unique DEPLOYMENT_ENV values, LiveKit may match an incoming call to the wrong trunk (one without a dispatch rule). This causes the call to be silently dropped with status: 486, reason: "flood".
The fix: Ensure each developer has a unique DEPLOYMENT_ENV value. The deterministic naming convention ensures each developer's resources are isolated.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Agent logs mode=console for inbound call | Dispatch rule metadata missing or corrupt | Re-activate inbound (deactivate + activate) to recreate dispatch rule |
| Call routes to wrong developer's agent | Same DEPLOYMENT_ENV on shared LiveKit | Use unique DEPLOYMENT_ENV per developer |
livekitInboundTrunkId: null in MongoDB | Trunk created before inbound support, or direction is "outbound" | Change direction to "both" createTrunk auto-creates the provider trunk |
providerKey: null for inbound trunk | Legacy trunk not yet migrated | Run migration script or re-activate inbound |
| Recording fails with "room does not exist" | Room deleted before recording started | Recording is best-effort; the error handler catches this |
Inbound call rejected with status: 486, reason: "flood" | Multiple trunks claim the same number, OR trunk deleted but dispatch rule still references it | Restart backend (trunk sync auto-heals) or re-activate inbound |
| Dispatch rule missing after LiveKit restart | LiveKit Redis data lost or LiveKit redeployed | Backend auto-recreates missing dispatch rules on startup via syncDispatchRules() |
| Agent receives wrong agent config | Static agent_id in old dispatch metadata | Re-activate inbound to get new per-provider dispatch rule with runtime resolution |
_resolve_inbound_number returns None | No active assignment for dialed number, or API timeout | Check PhoneNumberAssignment.inboundConfig.isActive and backend logs |
| Agent hot-swap fails ("failed to update session agent") | Session already closed or VoiceAssistant init error | Check Agent Server logs for detailed error; usually transient |
Migration Script
For production deployments with existing per-number trunks/rules, a one-off migration script was run. The original migrate-inbound-trunks.ts has since been removed after the migration completed; the current trunk migration script is migrate-sip-trunks.ts. The original invocation was:
cd client_backend
# Dry run see what would be migrated without making changes
npx tsx src/scripts/migrate-inbound-trunks.ts --dry-run
# Actual migration
npx tsx src/scripts/migrate-inbound-trunks.tsOutput:
{
"provider": "voicelink",
"numbersMigrated": 15,
"trunksRemoved": 14,
"rulesRemoved": 14,
"errors": []
}What it does:
- For each provider with legacy per-number trunks (
providerKey: null):- Creates or reuses the shared provider trunk via
getOrCreateProviderInboundTrunk() - Creates or reuses the shared dispatch rule via
getOrCreateProviderDispatchRule() - Updates all
PhoneNumberAssignmentdocs with the shareddispatchRuleId - Updates all
SIPTrunkdocs withlivekitInboundTrunkId,providerKey,deploymentEnv - Deletes legacy per-number LiveKit trunks (only AFTER verification succeeds)
- Creates or reuses the shared provider trunk via
Safety: If any step fails, legacy resources are left untouched. Re-running the script is idempotent.
Startup Sync (Resilience)
On every backend startup (and every minute via cron), syncAllTrunks() in trunkSync.service.ts runs three phases:
Outbound trunk sync Recreates missing outbound trunks from MongoDB config
Inbound trunk sync (per-provider) For each provider with ≥1 active inbound number:
- Computes expected trunk name:
{provider}-inbound-{deploymentEnv} - If 0 matching trunks on LiveKit → creates via
getOrCreateProviderInboundTrunk() - If 1 matching trunk → reconciles numbers to match all active inbound numbers for provider
- If 2+ matching trunks → duplicate collapse:
- Selects survivor (lowest
sipTrunkIdlexicographically) - Migrates dispatch rules from duplicates to survivor via
migrateDispatchRulesToTrunk() - Deletes duplicate trunks
- Selects survivor (lowest
- Updates all
SIPTrunkdocs for provider withlivekitInboundTrunkId,providerKey,deploymentEnv - Increments
report.providersReconciled
- Computes expected trunk name:
Dispatch rule sync (per-provider) For each provider with ≥1 active inbound number:
- Skips if no
livekitInboundTrunkIdyet (retry next run) - Computes expected room prefix:
inbound-{provider}-{deploymentEnv}- - If 0 matching rules → creates via
getOrCreateProviderDispatchRule(), incrementsreport.dispatchRulesRecreated - If 1 matching rule → no-op
- If 2+ matching rules → keeps first, deletes rest, increments
report.duplicatesRemoved - Writes the resolved
dispatchRuleIdonto ALL activePhoneNumberAssignmentdocs for that provider
- Skips if no
Failure safety: If listSipInboundTrunk() or listSipDispatchRule() fails, the entire phase is skipped and the error is logged. NO deletions happen based on incomplete LiveKit data (Requirement 9.6).
This makes the system resilient to:
- LiveKit server restarts (dispatch rules stored in Redis survive normal restarts)
- LiveKit Redis data loss (dispatch rules are recreated from MongoDB state)
- Multi-developer conflicts (stale trunks/rules from other
deploymentEnvvalues are NOT touched) - Backend restarts (no manual re-activation needed)
- Race conditions during concurrent activations (duplicates are collapsed automatically)
Changelog
2026-07 Per-Provider Model with Runtime Resolution
Problem: The previous per-number model created a separate LiveKit inbound trunk and dispatch rule for each phone number, with static agent_id in dispatch metadata. This caused:
- Multi-developer conflicts (486 "flood" errors)
- Stale agent identity when agents were reassigned
- Scalability issues with hundreds of LiveKit resources
Solution: Moved to a per-Provider model where:
- One shared inbound trunk per provider per deployment environment
- One shared dispatch rule per provider (NO per-number metadata)
- Agent identity resolved at runtime via new Inbound Resolution API
- Atomic uniqueness guard for agent-to-number linking
Key Changes:
| Component | Change |
|---|---|
SIPTrunk model | Added deploymentEnv, providerKey fields |
config/index.ts | Added deploymentEnv config value |
livekit.service.ts | Added deterministic naming helpers, getOrCreateProviderInboundTrunk(), getOrCreateProviderDispatchRule(), duplicate-collapse helpers |
telephony.service.ts | Reworked activateInbound() with atomic guard + shared trunk/rule reuse; deactivateInbound() no longer deletes shared resources |
trunkSync.service.ts | Per-provider reconciliation with duplicate collapse |
webhook.controller.ts | Added /internal/resolve-inbound-number endpoint; webhook reads SIP attributes instead of room name |
| Agent Server | Added _resolve_inbound_number() with bounded retries; skips sync_agent_settings() for inbound; hot-swaps session agent after resolution |
| Migration script | migrate-inbound-trunks.ts for existing deployments |
Backward Compatibility: The migration script handles existing per-number trunks. After migration, all inbound calls use runtime resolution.
2026-04-29 allowedAddresses Bugfix
Problem: Inbound calls from Vobiz SIP trunks failed because allowedAddresses was never passed to LiveKit when creating inbound SIP trunks.
Fix: All code paths that create inbound trunks now pass allowedAddresses from the trunk config. See SIP-TRUNK-SYSTEM.md for details.