Skip to content

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:

  1. 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)
  2. Scalability: Hundreds of phone numbers meant hundreds of LiveKit resources to manage
  3. Stale metadata: Static agent_id in dispatch rules became stale when agents were reassigned

The new per-Provider model solves these by:

  1. One trunk per provider per environment: {provider}-inbound-{deploymentEnv} (e.g., voicelink-inbound-production)
  2. One dispatch rule per provider: Shared across all numbers, with NO per-number metadata
  3. 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=production

Deterministic Resource Naming ​

LiveKit resources use deterministic names that any process can compute:

ResourceNaming ConventionExample
Inbound Trunk{provider}-inbound-{deploymentEnv}voicelink-inbound-production
Dispatch Rule Room Prefixinbound-{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:

  1. telephonyService.createTrunk() creates an outbound trunk on LiveKit
  2. 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
  3. Stores livekitTrunkId, livekitInboundTrunkId, providerKey, and deploymentEnv in the SIPTrunk MongoDB 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.

UI: AgentEditorPage.tsx → Inbound Number section

API: POST /api/customer/phone-numbers/:assignmentUID/activate-inbound

What happens (telephonyService.activateInbound()):

  1. Atomic uniqueness guard FIRST: Agent.findOneAndUpdate with condition inboundPhoneUID: null OR this phoneUID ensures only one agent can own a phone number. If this fails, NO further mutation happens (Requirement 6.3)
  2. Validates assignment direction supports inbound (not "outbound"-only)
  3. Computes union of all active inbound numbers for this provider via getProviderInboundNumbers()
  4. Calls getOrCreateProviderInboundTrunk() reuses existing shared trunk or creates it
  5. Calls getOrCreateProviderDispatchRule() reuses existing shared dispatch rule or creates it with minimal metadata:
    json
    { "mode": "inbound", "need_to_update_agent": true }
    Note: NO agent_id, phone_uid, orgUID, or customerUID in dispatch metadata these are resolved at runtime
  6. Updates PhoneNumberAssignment.inboundConfig with isActive: true, dispatchRuleId, defaultAgentId
  7. Updates SIPTrunk with livekitInboundTrunkId, 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:

  1. Reads the dialed number from SIP attributes NOT from the room name:
    javascript
    const dialedNumber = sipAttrs["sip.trunkPhoneNumber"] || sipAttrs["sip.phoneNumber"];
  2. Resolves the inbound assignment via phoneNumberAssignmentRepository.resolveInboundNumber(dialedNumber)
  3. If no active assignment found, logs a warning and skips Contact creation
  4. Checks org concurrency limits
  5. Creates a Contact document on-the-fly with:
    • contactUID: random nanoid (NOT the SIP identity)
    • roomId: the LiveKit room name
    • callDirection: "inbound"
    • campaignUID: inbound_{assignmentUID}_{YYYYMMDD}
    • AgentID, orgUID, customerUID: from the resolved assignment
    • s3RecordingKey: 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:

json
{ "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) ​

  1. resolve_metadata() parses ctx.job.metadata → detects mode: "inbound"
  2. Skips sync_agent_settings() for inbound mode agent identity isn't known yet
  3. Creates a placeholder AgentMetaSchema() for session bootstrap
  4. Enters the inbound mode handler → creates InboundCallHandler

5. InboundCallHandler (inbound_call_handler.py) ​

  1. Find SIP participant: Polls room for non-agent SIP participant (kind=3)
  2. Extract caller phone: From sip.phoneNumber attribute
  3. Extract dialed number: From sip.trunkPhoneNumber attribute (CRITICAL)
  4. Resolve agent identity via API (NEW):
    python
    resolution = await self._resolve_inbound_number(dialed_number)
    # Returns: { agentId, orgUID, customerUID, assignmentUID, greeting }
  5. 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")
  6. 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
  7. Compute campaign_id: inbound_{assignmentUID}_{YYYYMMDD} (uses assignmentUID from resolution)
  8. Resolve contactUID: Calls POST /resolve-inbound-contact to get the real contactUID
  9. Start recording: Calls POST /start-recording
  10. Say first message: Uses resolved agent_meta.first_message
  11. Store metadata: Updates ctx.userdata for 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:

json
{
  "dialedNumber": "+919876543210"
}

Headers:

x-api-secret: <WEBHOOK_SECRET>

Success Response (200):

json
{
  "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 defaultAgentId configured
json
{
  "success": false,
  "message": "No active inbound configuration for this number"
}

Implementation: webhook.controller.ts → resolveInboundNumber()

Schema Changes ​

SIPTrunk Model ​

typescript
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 label

Note: inboundConfig no longer lives on PhoneNumber. It's on PhoneNumberAssignment.

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:     Date

Agent Model ​

inboundPhoneUID: String | null      // Phone number linked for inbound (unique sparse index)

Contact Model ​

callDirection: "outbound" | "inbound"  // Distinguishes call type

File Changes Summary ​

Agent Server (Python) ​

FileChangeWhy
agent.pySkips sync_agent_settings() for mode == "inbound"Agent identity unknown until runtime resolution; uses placeholder AgentMetaSchema()
agent.pyAdded inbound mode handlerRoutes to InboundCallHandler
src/handlers/inbound_call_handler.pyAdded _resolve_inbound_number()Calls /internal/resolve-inbound-number API with bounded retries
src/handlers/inbound_call_handler.pyUpdated start_call()Reads sip.trunkPhoneNumber, resolves agent via API, hot-swaps session agent
src/schemas/metadata_schema.pyextra = 'allow' already setAccepts dispatch metadata fields
requirements-dev.txtAdded hypothesis>=6.100.0Property-based testing support

Client Backend (Node.js) ​

FileChangeWhy
config/index.tsAdded deploymentEnv configDistinguishes resources across environments
models/SIPTrunk.tsAdded deploymentEnv, providerKey fieldsPer-provider model support
services/livekit.service.tsAdded inboundTrunkName(), providerRoomPrefix()Deterministic naming helpers
services/livekit.service.tsAdded getOrCreateProviderInboundTrunk()Idempotent lookup-by-name-then-create
services/livekit.service.tsAdded getOrCreateProviderDispatchRule()Shared dispatch rule with NO per-number metadata
services/livekit.service.tsAdded migrateDispatchRulesToTrunk(), selectDuplicateTrunkSurvivor()Duplicate-collapse helpers for trunk sync
services/livekit.service.tsReworked cleanupStaleInboundTrunks()Name-driven (not DB-driven) stale trunk cleanup
services/telephony.service.tsAdded getProviderInboundNumbers()Computes union of numbers across all customers for a provider
services/telephony.service.tsReworked activateInbound()Atomic uniqueness guard FIRST, then shared trunk/rule reuse
services/telephony.service.tsReworked updateInboundAgent()Same atomic guard, reuses existing dispatch rule
services/telephony.service.tsReworked deactivateInbound()Clears agent link, does NOT delete shared trunk/rule
services/telephony.service.tsRemoved ensureInboundTrunkExists()Replaced by getOrCreateProviderInboundTrunk()
services/trunkSync.service.tsAdded getActiveInboundProviders()Lists providers with ≥1 active inbound number
services/trunkSync.service.tsReworked syncInboundTrunks()Per-provider reconciliation with duplicate collapse
services/trunkSync.service.tsReworked syncDispatchRules()Per-provider reconciliation, writes dispatchRuleId to all active assignments
services/trunkSync.service.tsAdded duplicatesRemoved, providersReconciled to SyncReportNew sync metrics
controllers/webhook.controller.tsUpdated participant_joined for inboundReads sip.trunkPhoneNumber from SIP attributes, resolves via resolveInboundNumber()
controllers/webhook.controller.tsAdded resolveInboundNumber controller/internal/resolve-inbound-number endpoint
routes/webhook.routes.tsAdded /internal/resolve-inbound-number routeNew internal API endpoint
scripts/migrate-inbound-trunks.tsNew fileMigration script for legacy per-number trunks

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) ​

json
{
  "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) ​

json
{
  "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:

  1. Reads sip.trunkPhoneNumber from the SIP participant's attributes
  2. Calls POST /internal/resolve-inbound-number to get the real agent identity
  3. Fetches the resolved agent's configuration
  4. Hot-swaps the session agent with the resolved config

Inbound vs Outbound: Key Differences ​

AspectOutboundInbound
Who initiatesAgent calls create_sip_participant()Caller dials the number, LiveKit creates room
SIP participantCreated by agentAlready in room when agent joins
Contact creationBefore call starts (campaign flow)On-the-fly by webhook handler
contactUID sourceSet during campaign dispatchRandom nanoid from webhook
Agent identityKnown at dispatch timeResolved at runtime via API
Recording triggerAgent calls /start-recording after SIP answersAgent calls /start-recording after joining room
First messageagent_meta.first_messageagent_meta.first_message (resolved agent)
Campaign IDFrom campaign documentinbound_{assignmentUID}_{YYYYMMDD}
Room nameroom-{uuid}inbound-{provider}-{env}-{random}
Trunk modelPer-trunk (outbound only)Per-provider (shared across all numbers)
Dispatch rulePer-campaignShared per-provider (no agent identity in metadata)

Multi-Developer Environment ​

When multiple developers share the same LiveKit server:

  1. Each developer MUST use a unique DEPLOYMENT_ENV (or AGENT_NAME as fallback) in .env:

    bash
    DEPLOYMENT_ENV=dev-alice  # Recommended
    # or
    AGENT_NAME=Oktivo-Dev-Alice  # Used as fallback if DEPLOYMENT_ENV not set
  2. The 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-)
  3. 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.

  4. 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 PhoneNumberAssignment docs with the correct dispatchRuleId

Environment Variable Reference ​

VariableDescriptionExample
DEPLOYMENT_ENVPrimary deployment environment identifierproduction, dev-alice, staging
AGENT_NAMELiveKit agent name (also used as deploymentEnv fallback)Oktivo-Dev-Alice
NODE_ENVIf 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 ​

SymptomCauseFix
Agent logs mode=console for inbound callDispatch rule metadata missing or corruptRe-activate inbound (deactivate + activate) to recreate dispatch rule
Call routes to wrong developer's agentSame DEPLOYMENT_ENV on shared LiveKitUse unique DEPLOYMENT_ENV per developer
livekitInboundTrunkId: null in MongoDBTrunk created before inbound support, or direction is "outbound"Change direction to "both" createTrunk auto-creates the provider trunk
providerKey: null for inbound trunkLegacy trunk not yet migratedRun migration script or re-activate inbound
Recording fails with "room does not exist"Room deleted before recording startedRecording 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 itRestart backend (trunk sync auto-heals) or re-activate inbound
Dispatch rule missing after LiveKit restartLiveKit Redis data lost or LiveKit redeployedBackend auto-recreates missing dispatch rules on startup via syncDispatchRules()
Agent receives wrong agent configStatic agent_id in old dispatch metadataRe-activate inbound to get new per-provider dispatch rule with runtime resolution
_resolve_inbound_number returns NoneNo active assignment for dialed number, or API timeoutCheck PhoneNumberAssignment.inboundConfig.isActive and backend logs
Agent hot-swap fails ("failed to update session agent")Session already closed or VoiceAssistant init errorCheck Agent Server logs for detailed error; usually transient

Migration Script ​

For production deployments with existing per-number trunks/rules, run the migration script:

bash
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.ts

Output:

json
{
  "provider": "voicelink",
  "numbersMigrated": 15,
  "trunksRemoved": 14,
  "rulesRemoved": 14,
  "errors": []
}

What it does:

  1. 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 PhoneNumberAssignment docs with the shared dispatchRuleId
    • Updates all SIPTrunk docs with livekitInboundTrunkId, providerKey, deploymentEnv
    • Deletes legacy per-number LiveKit trunks (only AFTER verification succeeds)

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:

  1. Outbound trunk sync Recreates missing outbound trunks from MongoDB config

  2. 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 sipTrunkId lexicographically)
      • Migrates dispatch rules from duplicates to survivor via migrateDispatchRulesToTrunk()
      • Deletes duplicate trunks
    • Updates all SIPTrunk docs for provider with livekitInboundTrunkId, providerKey, deploymentEnv
    • Increments report.providersReconciled
  3. Dispatch rule sync (per-provider) For each provider with ≥1 active inbound number:

    • Skips if no livekitInboundTrunkId yet (retry next run)
    • Computes expected room prefix: inbound-{provider}-{deploymentEnv}-
    • If 0 matching rules → creates via getOrCreateProviderDispatchRule(), increments report.dispatchRulesRecreated
    • If 1 matching rule → no-op
    • If 2+ matching rules → keeps first, deletes rest, increments report.duplicatesRemoved
    • Writes the resolved dispatchRuleId onto ALL active PhoneNumberAssignment docs for that provider

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 deploymentEnv values 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:

ComponentChange
SIPTrunk modelAdded deploymentEnv, providerKey fields
config/index.tsAdded deploymentEnv config value
livekit.service.tsAdded deterministic naming helpers, getOrCreateProviderInboundTrunk(), getOrCreateProviderDispatchRule(), duplicate-collapse helpers
telephony.service.tsReworked activateInbound() with atomic guard + shared trunk/rule reuse; deactivateInbound() no longer deletes shared resources
trunkSync.service.tsPer-provider reconciliation with duplicate collapse
webhook.controller.tsAdded /internal/resolve-inbound-number endpoint; webhook reads SIP attributes instead of room name
Agent ServerAdded _resolve_inbound_number() with bounded retries; skips sync_agent_settings() for inbound; hot-swaps session agent after resolution
Migration scriptmigrate-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.

Last updated: