how can i change the default language of the agent
# ask-ai
l
how can i change the default language of the 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 — I’m responding to your question about how to change the agent’s default language. Short answer: the agent doesn’t have one global default language — you set the language per component inside the AgentSession (STT, TTS, and LLM). You can do this in one of three ways depending on how you instantiate models. 1) Using model descriptor strings (shortcut) • Pass
provider/model:language
for
stt
,
tts
, and
llm
when creating the AgentSession. Example (Python)
Copy code
from livekit.agents import AgentSession

session = AgentSession(
    stt="deepgram/nova-3:es",    # Spanish for STT
    tts="cartesia/sonic-3:esp",  # Spanish for TTS
    llm="openai/gpt-4o:es"       # Spanish for LLM
)
2) Using model objects (explicit) • Create
STT
,
TTS
, or
LLM
instances from
inference
and pass a
language
parameter. Example (Python)
Copy code
from livekit.agents import AgentSession, inference

session = AgentSession(
    stt=inference.STT(model="deepgram/nova-3", language="es"),
    tts=inference.TTS(model="cartesia/sonic-3", language="esp"),
    llm=inference.LLM(model="openai/gpt-4o", language="es"),
)
3) Using provider plugins (provider-specific classes) • Use the plugin’s class and set
language
there. Example (Python with Deepgram plugin)
Copy code
from livekit.plugins import deepgram

session = AgentSession(
    stt=deepgram.STT(model="nova-3", language="es"),
    # tts / llm as needed
)
Important notes • Language codes can vary by provider (e.g., Deepgram uses
es
, some TTS providers use
esp
). Use the provider’s supported codes for each model. • For multilingual STT support, some models accept
language="multi"
and you can also switch languages at runtime with
update_options()
if your workflow needs dynamic switching. Would you like: • Examples for Node.js instead of Python? • Help picking the correct language code for a specific provider/model? • An example of switching languages at runtime? Sources: Speech-to-text (STT) models overview | LiveKit Documentation | Cartesia TTS | LiveKit Documentation | ElevenLabs TTS | LiveKit Documentation | Deepgram STT | LiveKit Documentation | Build a multilingual voice agent that automatically switches languages | Li...
l
examples for nodejs
t
Thanks — I’m following up on your request for Node.js examples to change an agent’s language. Below are concise examples for the three methods: descriptor strings, model objects, and provider plugins. I also included notes about language codes and runtime switching. 1) Using model descriptor strings (shortcut) • Pass
provider/model:language
for
stt
,
tts
, and
llm
when creating the AgentSession. Example (Node.js / TypeScript)
Copy code
import { AgentSession } from '@livekit/agents';

const session = new AgentSession({
  stt: "deepgram/nova-3:es",      // Spanish for STT
  tts: "cartesia/sonic-3:esp",    // Spanish for TTS (provider-specific code)
  llm: "openai/gpt-4o:es"         // Spanish for LLM
});
2) Using model objects (explicit) • Create
STT
,
TTS
, or
LLM
instances from
inference
and specify
language
. Example (Node.js / TypeScript)
Copy code
import { AgentSession, inference } from '@livekit/agents';

const session = new AgentSession({
  stt: new inference.STT({ model: "deepgram/nova-3", language: "es" }),
  tts: new inference.TTS({ model: "cartesia/sonic-3", language: "esp" }),
  llm: new inference.LLM({ model: "openai/gpt-4o", language: "es" })
});
3) Using provider plugins (provider-specific classes) • Use the plugin’s STT/TTS class and set
language
there. Example (Node.js)
Copy code
import { AgentSession } from '@livekit/agents';
import { deepgram } from '@livekit/plugins-deepgram';

const session = new AgentSession({
  stt: new deepgram.STT({ model: "nova-3", language: "es" }),
  // tts / llm can be set similarly using plugin classes
});
Key notes • Language codes vary by provider: use the provider’s supported code for each model (e.g., Deepgram uses `es`; some TTS providers use
esp
). • There is no single global agent language — set the language per component (STT, TTS, LLM). • For multilingual STT, some models support
language: "multi"
. You can also change languages at runtime using
update_options()
on the session if you need dynamic switching. Would you like a full runnable Node.js snippet showing how to start the session and switch languages at runtime?
l
const session = new voice.AgentSession({ stt: streamAdapter, llm: new google.LLM({ model: 'gemini-3-flash-preview', apiKey: '{{GOOGLE_API_KEY}}', }), tts: new elevenlabs.TTS({ voice: { id: 'JBFqnCBsd6RMkjVDRZzb', name: '', category: '', }, model: 'eleven_multilingual_v2', apiKey: '{{ELEVENLABS_API_KEY}}', }), vad: vad, }); i have a custom stt which is infomaniak
t
Thanks — I’m responding to your snippet and your note that you have a custom Infomaniak STT. I can help you integrate it and set the language for STT in your AgentSession. A few quick questions so I give the right example: • Do you have an SDK or an HTTP API for Infomaniak STT (or is it a local/custom process)? • How do you currently send audio to
streamAdapter
— is it already converting microphone audio into the format Infomaniak expects, or do you need help wiring that too? • Does Infomaniak accept a language parameter (e.g.,
language: 'fr'
) in requests, or do you need language selection handled client-side? While you answer, here are two common approaches you can use depending on how your Infomaniak STT is available. 1) Wrap Infomaniak as a custom STT implementation (recommended if you have an SDK or can call an HTTP API) • Create a class that implements the interface expected by the voice/agents STT adapter (accepts audio chunks and returns transcriptions). In that class include a
language
option and pass that option to Infomaniak requests if the API supports it. • Use that wrapper instance in place of
streamAdapter
when creating the AgentSession. Sketch (Node.js/TypeScript)
Copy code
// pseudocode — adapt to your SDK/API
class InfomaniakSTT {
  constructor(opts: { apiKey: string; language?: string }) {
    this.apiKey = opts.apiKey;
    this.language = opts.language ?? 'fr';
  }

  // called by the voice agent code to push audio
  async acceptAudioChunk(chunk: ArrayBuffer) {
    // buffer/convert chunk as Infomaniak requires
    // send to Infomaniak API with language=this.language
    // handle streaming or batch response
  }

  // return transcription text (or stream results as your adapter expects)
  async getTranscription() { /* ... */ }
}

// then
const infomaniakAdapter = new InfomaniakSTT({ apiKey: process.env.INFOMANIAK_KEY, language: 'fr' });

const session = new voice.AgentSession({
  stt: infomaniakAdapter,
  llm: new google.LLM({
    model: 'gemini-3-flash-preview',
    apiKey: process.env.GOOGLE_API_KEY,
    language: 'fr'           // set LLM language if supported by this wrapper
  }),
  tts: new elevenlabs.TTS({
    voice: { id: 'JBFqnCBsd6RMkjVDRZzb' },
    model: 'eleven_multilingual_v2',
    apiKey: process.env.ELEVENLABS_API_KEY,
    language: 'fr'           // provider-specific code (if supported)
  }),
  vad
});
2) Use an adapter that translates LiveKit audio stream to Infomaniak’s HTTP endpoints (useful if Infomaniak expects full audio files) • Buffer audio from the live stream into short segments (e.g., 5–10s) and POST them to Infomaniak with a language field. • Emit returned transcriptions back into the agent as if they came from a streaming STT adapter. Notes about language handling • The AgentSession itself doesn’t provide a single global language — set language on each component (STT, TTS, LLM). • If Infomaniak supports language codes, pass the appropriate code from your wrapper when you call their API. If not, perform language detection or choose a code client-side and send text to the LLM/TTS in that language. • ElevenLabs and Google may each require different language identifiers. Ensure you use provider-specific codes for TTS/LLM where applicable. If you share: • a sample of Infomaniak’s STT request/response (or SDK method names), and • how
streamAdapter
is currently implemented (or whether you want me to replace it), I’ll produce a concrete, runnable Node.js example that wires Infomaniak into your AgentSession and demonstrates setting/changing the language (including runtime switching with an
update_options()
-style approach if you want that).
l
const streamAdapter = new stt.StreamAdapter(infomaniakSTT as stt.STT, vad); const infomaniakSTT = new InfomaniakSTT({ apiToken: process.env.INFOMANIAK_API_TOKEN!, productId: process.env.INFOMANIAK_PRODUCT_ID!, pollingInterval: 500, maxPollingAttempts: 60, }); // infomaniak-stt.ts import { type AudioBuffer, mergeFrames, stt } from '@livekit/agents'; import type { AudioFrame } from '@livekit/rtc-node'; import axios from 'axios'; import dotenv from 'dotenv'; import FormData from 'form-data'; export interface InfomaniakSTTOptions { apiToken: string; productId: string; baseURL?: string; pollingInterval?: number; maxPollingAttempts?: number; language?: string; } interface TranscriptionResponse { batch_id: string; } interface BatchStatusResponse { status: 'pending' | 'processing' | 'success' | 'failed'; url?: string; file_name?: string; file_size?: number; data?: string; } const defaultInfomaniakSTTOptions: Partial<InfomaniakSTTOptions> = { baseURL: 'https://api.infomaniak.com', pollingInterval: 500, maxPollingAttempts: 60, language: 'en', }; dotenv.config({ path: '.env.local' }); export class InfomaniakSTT extends stt.STT { #opts: InfomaniakSTTOptions; label = 'infomaniak.STT'; /** * @remarks *
apiToken
must be set to your Infomaniak API token, either using the argument or by setting the *
INFOMANIAK_API_TOKEN
environment variable. *
productId
must be set to your Infomaniak product ID, either using the argument or by setting the *
INFOMANIAK_PRODUCT_ID
environment variable. */ constructor(opts: Partial<InfomaniakSTTOptions> = {}) { super({ streaming: false, interimResults: false }); this.#opts = { apiToken: opts.apiToken || process.env.INFOMANIAK_API_TOKEN || '', productId: opts.productId || process.env.INFOMANIAK_PRODUCT_ID || '', baseURL: opts.baseURL || defaultInfomaniakSTTOptions.baseURL!, pollingInterval: opts.pollingInterval || defaultInfomaniakSTTOptions.pollingInterval!, maxPollingAttempts: opts.maxPollingAttempts || defaultInfomaniakSTTOptions.maxPollingAttempts!, language: opts.language || defaultInfomaniakSTTOptions.language!, }; if (!this.#opts.apiToken) { throw new Error( 'Infomaniak API token is required, whether as an argument or as $INFOMANIAK_API_TOKEN', ); } if (!this.#opts.productId) { throw new Error( 'Infomaniak Product ID is required, whether as an argument or as $INFOMANIAK_PRODUCT_ID', ); } } async _recognize(buffer: AudioBuffer): Promise<stt.SpeechEvent> { try { // Merge all audio frames into a single frame const mergedBuffer = mergeFrames(buffer); // Convert to WAV format const wavBuffer = this.#createWav(mergedBuffer); // Upload and create transcription batch const batchId = await this.#createTranscription(wavBuffer); // Poll for completion const transcriptionText = await this.#pollForResult(batchId); // Return speech event return { type: stt.SpeechEventType.FINAL_TRANSCRIPT, alternatives: [ { text: transcriptionText, language: '', startTime: 0, endTime: 0, confidence: 1.0, }, ], }; } catch (error) { console.error('Infomaniak STT error:', error); throw error; } } /** * Create WAV file header and combine with audio data */ #createWav(frame: AudioFrame): Buffer { const bitsPerSample = 16; const byteRate = (frame.sampleRate * frame.channels * bitsPerSample) / 8; const blockAlign = (frame.channels * bitsPerSample) / 8; const header = Buffer.alloc(44); // RIFF chunk descriptor header.write('RIFF', 0); header.writeUInt32LE(36 + frame.data.byteLength, 4); header.write('WAVE', 8); // fmt sub-chunk header.write('fmt ', 12); header.writeUInt32LE(16, 16); // Subchunk1Size header.writeUInt16LE(1, 20); // AudioFormat (1 = PCM) header.writeUInt16LE(frame.channels, 22); header.writeUInt32LE(frame.sampleRate, 24); header.writeUInt32LE(byteRate, 28); header.writeUInt16LE(blockAlign, 32); header.writeUInt16LE(16, 34); // BitsPerSample // data sub-chunk header.write('data', 36); header.writeUInt32LE(frame.data.byteLength, 40); return Buffer.concat([header, Buffer.from(frame.data.buffer)]); } /** * Create transcription batch on Infomaniak */ async #createTranscription(audioBuffer: Buffer): Promise<string> { const form = new FormData(); form.append('file', audioBuffer, { filename: 'audio.wav', contentType: 'audio/wav', }); form.append('model', 'whisper'); const url = `${this.#opts.baseURL}/1/ai/${this.#opts.productId}/openai/audio/transcriptions`; try { const response = await axios.post<TranscriptionResponse>(url, form, { headers: { Authorization:
Bearer ${this.#opts.apiToken}
, ...form.getHeaders(), }, }); if (!response.data.batch_id) { throw new Error('No batch_id received from Infomaniak API'); } console.log(
[InfomaniakSTT] Transcription batch created: ${response.data.batch_id}
); return response.data.batch_id; } catch (error) { if (axios.isAxiosError(error)) { console.error('[InfomaniakSTT] Axios error details:', { message: error.message, code: error.code, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, headers: error.response?.headers, }); } else { console.error('[InfomaniakSTT] Non-axios error:', error); } throw new Error(
Failed to create transcription: ${error instanceof Error ? error.message : String(error)}
, ); } } /** * Poll for transcription result */ async #pollForResult(batchId: string): Promise<string> { const url = `${this.#opts.baseURL}/1/ai/${this.#opts.productId}/results/${batchId}`; for (let attempt = 0; attempt < (this.#opts.maxPollingAttempts as number); attempt++) { try { const response = await axios.get<BatchStatusResponse>(url, { headers: { Authorization:
Bearer ${this.#opts.apiToken}
, }, }); const status = response.data.status; console.log(
[InfomaniakSTT] Batch ${batchId} status: ${status} (attempt ${attempt + 1}/${this.#opts.maxPollingAttempts})
, ); if (status === 'success') { return await this.#downloadResult(batchId, response.data); } else if (status === 'failed') { throw new Error(
Transcription batch ${batchId} failed
); } await this.#sleep(this.#opts.pollingInterval as number); } catch (error) { if (axios.isAxiosError(error)) { console.error(
[InfomaniakSTT] Error polling batch ${batchId}:
, error.response?.data || error.message, ); } throw error; } } throw new Error(
Transcription timed out after ${this.#opts.maxPollingAttempts} attempts (${((this.#opts.maxPollingAttempts as number) * (this.#opts.pollingInterval as number)) / 1000}s)
, ); } /** * Download transcription result */ async #downloadResult(batchId: string, statusData: BatchStatusResponse): Promise<string> { // Data is directly in the status response if (statusData.data) { console.log(
[InfomaniakSTT] Transcription received in status response
); return this.#parseTranscriptionData(statusData.data); } // Download from provided URL if (statusData.url) { try { console.log(
[InfomaniakSTT] Downloading from URL: ${statusData.url}
); const response = await axios.get(statusData.url, { headers: { Authorization:
Bearer ${this.#opts.apiToken}
, }, }); return this.#parseTranscriptionData(response.data); } catch (error) { console.error('[InfomaniakSTT] Error downloading from URL:', error); } } // Try the download endpoint const downloadUrl = `${this.#opts.baseURL}/1/ai/${this.#opts.productId}/results/${batchId}/download`; try { console.log(
[InfomaniakSTT] Downloading from endpoint: ${downloadUrl}
); const response = await axios.get(downloadUrl, { headers: { Authorization:
Bearer ${this.#opts.apiToken}
, }, }); return this.#parseTranscriptionData(response.data); } catch (error) { if (axios.isAxiosError(error)) { console.error( '[InfomaniakSTT] Error downloading from endpoint:', error.response?.data || error.message, ); } throw error; } } /** * Parse transcription data from various possible response formats */ #parseTranscriptionData(data: any): string { // Handle string response if (typeof data === 'string') { return data; } // Handle object with 'text' property if (data && typeof data === 'object' && data.text) { return data.text; } // Handle object with 'transcription' property if (data && typeof data === 'object' && data.transcription) { return data.transcription; } // If it's an object, try to stringify and use as text if (data && typeof data === 'object') { console.warn('[InfomaniakSTT] Unexpected response format, using JSON string:', data); return JSON.stringify(data); } throw new Error(
Unexpected transcription data format: ${JSON.stringify(data)}
); } /** * Sleep utility for polling */ #sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * This method throws an error; streaming is unsupported on Infomaniak STT. */ stream(): stt.SpeechStream { throw new Error('Streaming is not supported on Infomaniak STT'); } }
t
Thanks — I’m responding to your Infomaniak STT implementation and your goal to set the STT language in your AgentSession. Your adapter looks solid. Below are focused, actionable steps and a small code change to make language configurable and surface it in the `StreamAdapter`/AgentSession so the whole agent uses the intended language. Summary of what to do • Add a
language
option to your InfomaniakSTT class (you already have it in the options). Ensure that option is sent to Infomaniak when creating the transcription batch. • Pass the desired language when you construct InfomaniakSTT and set LLM/TT S language consistently in the AgentSession. • Optionally support runtime language changes by exposing a setter or
updateOptions()
that updates
this.#opts.language
. Concrete changes 1) Send the language when creating the transcription request In your #createTranscription method you currently append
model: 'whisper'
. Add the language param to the form if Infomaniak supports it:
Copy code
// inside #createTranscription
form.append('model', 'whisper');

// add language field from this.#opts.language
if (this.#opts.language) {
  form.append('language', this.#opts.language);
}
This ensures Infomaniak receives the requested language (e.g., 'fr', 'en') with the upload. 2) Pass the language when constructing InfomaniakSTT When creating the instance, include language explicitly so it doesn’t rely only on defaults:
Copy code
const infomaniakSTT = new InfomaniakSTT({
  apiToken: process.env.INFOMANIAK_API_TOKEN!,
  productId: process.env.INFOMANIAK_PRODUCT_ID!,
  pollingInterval: 500,
  maxPollingAttempts: 60,
  language: 'fr' // set desired language here
});
const streamAdapter = new stt.StreamAdapter(infomaniakSTT as stt.STT, vad);

const session = new voice.AgentSession({
  stt: streamAdapter,
  llm: new google.LLM({
    model: 'gemini-3-flash-preview',
    apiKey: process.env.GOOGLE_API_KEY,
    language: 'fr' // set LLM language where supported
  }),
  tts: new elevenlabs.TTS({
    voice: { id: 'JBFqnCBsd6RMkjVDRZzb' },
    model: 'eleven_multilingual_v2',
    apiKey: process.env.ELEVENLABS_API_KEY,
    language: 'fr' // provider-specific code if supported
  }),
  vad
});
3) Expose runtime language updates (optional) If you need to change language during a session, add a method on InfomaniakSTT to update the option:
Copy code
setLanguage(lang: string) {
  this.#opts.language = lang;
}
Then call it from your app when you want to switch languages:
Copy code
infomaniakSTT.setLanguage('en');
If you want the entire AgentSession to switch languages at runtime, call the provider-specific update for LLM/TT S as supported by their SDKs (or recreate those components) and update the STT adapter as above. 4) Small robustness suggestions • Validate the language code early and document which codes Infomaniak expects (e.g., 'fr', 'en'). If Infomaniak uses different codes, map them in your wrapper. • When polling, log the language with each request (helps debug wrong-language transcriptions). • If Infomaniak returns language metadata in the response, populate the
language
field in your returned SpeechEvent alternative:
Copy code
alternatives: [{
  text: transcriptionText,
  language: this.#opts.language || '',
  ...
}]
Questions to confirm next steps • Does Infomaniak accept a
language
field in the transcription POST (your API docs)? If not, we should implement client-side selection or detection before sending audio. • Do you want an example that shows runtime switching of language across STT, LLM, and TTS together? If you confirm Infomaniak accepts
language
in the POST, I can provide a small patched file showing the exact insertion points and a runnable Node.js snippet that sets and changes language at runtime.
l
i want to fix the deafult language as italian and not change it based on users choice
t
Thanks — I’m responding to your request to lock the default language to Italian for your Infomaniak STT and the rest of the agent (LLM and TTS), so it never changes based on user choice. What to do (summary) • Hard-code the language to Italian in your InfomaniakSTT options and ensure that value is sent with every transcription request. • Set the
language
option for your LLM and TTS instances to Italian when constructing the AgentSession. • Remove any runtime language-update paths (or simply don't call them) so nothing in your app changes language after startup. • Populate the returned
SpeechEvent
language field so downstream code sees the language is Italian. Concrete changes for your code 1) Default and constructor: force Italian Change the default language and ensure the constructor always uses it (no user-overridable fallback):
Copy code
const defaultInfomaniakSTTOptions: Partial<InfomaniakSTTOptions> = {
  baseURL: '<https://api.infomaniak.com>',
  pollingInterval: 500,
  maxPollingAttempts: 60,
  language: 'it', // set Italian as the default
};

constructor(opts: Partial<InfomaniakSTTOptions> = {}) {
  super({ streaming: false, interimResults: false });

  this.#opts = {
    apiToken: opts.apiToken || process.env.INFOMANIAK_API_TOKEN || '',
    productId: opts.productId || process.env.INFOMANIAK_PRODUCT_ID || '',
    baseURL: opts.baseURL || defaultInfomaniakSTTOptions.baseURL!,
    pollingInterval: opts.pollingInterval || defaultInfomaniakSTTOptions.pollingInterval!,
    maxPollingAttempts: opts.maxPollingAttempts || defaultInfomaniakSTTOptions.maxPollingAttempts!,
    language: 'it', // force Italian regardless of incoming opts
  };
  ...
}
If you prefer to allow configuration via env variable but disallow runtime changes, use:
Copy code
language: opts.language || process.env.INFOMANIAK_LANGUAGE || 'it',
and do not expose a
setLanguage
method. 2) Send language to Infomaniak with every transcription In #createTranscription append
language
to the form so Infomaniak always receives Italian:
Copy code
form.append('model', 'whisper');
form.append('language', this.#opts.language); // will be 'it'
3) Return language in SpeechEvent When returning the final transcript, set the
language
field so downstream components know it’s Italian:
Copy code
return {
  type: stt.SpeechEventType.FINAL_TRANSCRIPT,
  alternatives: [{
    text: transcriptionText,
    language: this.#opts.language || 'it',
    startTime: 0,
    endTime: 0,
    confidence: 1.0,
  }],
};
4) Construct AgentSession with Italian for LLM and TTS Set LLM and TTS language at creation. Use provider-specific codes (Google may accept
it
, ElevenLabs may use
it
or another identifier depending on the model):
Copy code
const infomaniakSTT = new InfomaniakSTT({
  apiToken: process.env.INFOMANIAK_API_TOKEN!,
  productId: process.env.INFOMANIAK_PRODUCT_ID!,
  language: 'it', // Italian fixed
  pollingInterval: 500,
  maxPollingAttempts: 60,
});

const streamAdapter = new stt.StreamAdapter(infomaniakSTT as stt.STT, vad);

const session = new voice.AgentSession({
  stt: streamAdapter,
  llm: new google.LLM({
    model: 'gemini-3-flash-preview',
    apiKey: process.env.GOOGLE_API_KEY,
    language: 'it', // LLM language fixed to Italian
  }),
  tts: new elevenlabs.TTS({
    voice: { id: 'JBFqnCBsd6RMkjVDRZzb' },
    model: 'eleven_multilingual_v2',
    apiKey: process.env.ELEVENLABS_API_KEY,
    language: 'it', // set to provider-specific Italian code if required
  }),
  vad,
});
5) Remove or avoid runtime language switching • Do not call any
setLanguage
,
updateOptions()
, or similar methods that would alter language at runtime. • If you previously added a
setLanguage
on InfomaniakSTT for testing, remove it or ensure it is never invoked. 6) Verify provider language codes • Confirm Infomaniak expects
it
(or another code) for the transcription endpoint. If their API uses a different Italian code, use that. • Confirm Google and ElevenLabs accept
it
for their
language
option; if a provider expects a different code (e.g.,
ita
), adjust accordingly. Optional: add guard-rails • Log and assert language at startup to ensure all components are Italian:
Copy code
console.log('Agent languages: STT=', infomaniakSTT.#opts.language, 'LLM=it', 'TTS=it');
• Return an error or throw if any component is constructed without the expected Italian language to prevent misconfiguration. Do you want me to: • Patch your InfomaniakSTT file with the exact diffs applied, or • Provide a complete runnable Node.js snippet (including imports and environment setup) that demonstrates the fixed-Italian configuration?
l
2026-03-05T205910.365Z INFO sip sip/media_port.go:287 media timeout loop stopped {"nodeID": "NE_sUHoHeutDQ3V", "sipTrunk": "ST_NjwdW22CmFhc", "callID": "SCL_t2m3WHeSY95H", "traceID": "74326d3357486553", "room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744335333", "participant": "sip-participant", "participantName": "", "fromHost": "", "fromUser": "+18153489346", "toHost": "nsg.pstn.twilio.com", "toUser": "+917478886371", "jitterBuf": false} 2026-03-05T205910.365Z INFO sip service/service.go:258 SIP call ended {"nodeID": "NE_sUHoHeutDQ3V", "callID": "SCL_t2m3WHeSY95H", "reason": "invite-failed"} 2026-03-05T205910.451Z DEBUG Unhandled sip response. UnhandledResponseHandler handler not added {"nodeID": "NE_sUHoHeutDQ3V", "caller": "transaction.Layer", "msg": "response status=200 reason=canceling transport=UDP source=54.172.60.3:5060"} 2026-03-05T205910.453Z DEBUG Unhandled sip response. UnhandledResponseHandler handler not added {"nodeID": "NE_sUHoHeutDQ3V", "caller": "transaction.Layer", "msg": "response status=487 reason=Request Terminated transport=UDP source=54.172.60.3:5060"} 2026-03-05T205910.911Z DEBUG Unhandled sip response. UnhandledResponseHandler handler not added {"nodeID": "NE_sUHoHeutDQ3V", "caller": "transaction.Layer", "msg": "response status=487 reason=Request Terminated transport=UDP source=54.172.60.3:5060"} 2026-03-05T205911.910Z DEBUG Unhandled sip response. UnhandledResponseHandler handler not added {"nodeID": "NE_sUHoHeutDQ3V", "caller": "transaction.Layer", "msg": "response status=487 reason=Request Terminated transport=UDP source=54.172.60.3:5060"} 2026-03-05T205913.911Z DEBUG Unhandled sip response. UnhandledResponseHandler handler not added {"nodeID": "NE_sUHoHeutDQ3V", "caller": "transaction.Layer", "msg": "response status=487 reason=Request Terminated transport=UDP source=54.172.60.3:5060"} what do these logs point towatds (livekit sip)
t
Thanks — I’m responding to your LiveKit SIP logs and what they indicate. Summary • The call attempt ended because the initial SIP INVITE failed and the session was terminated (reason: "invite-failed"). • The important responses in the logs are a 200 with reason "canceling" and several 487 "Request Terminated" messages. A 487 means the INVITE transaction was canceled before the call was established. What those entries mean, practically • media timeout loop stopped — the media watchdog for that call stopped because the call session is shutting down. • SIP call ended {"reason":"invite-failed"} — the call setup did not complete (the INVITE never succeeded). • response status=200 reason=canceling — the remote endpoint acknowledged a cancel flow (often seen when a CANCEL was sent and the remote side replies). • response status=487 reason=Request Terminated — the final indication that the INVITE was terminated/canceled and the call will not be established. Likely causes to investigate • The caller or your system sent a CANCEL (intentional hangup or a local timeout) before the call completed. • The PSTN gateway / SIP trunk (nsg.pstn.twilio.com) or destination number rejected or canceled the call quickly (user hangup, busy, unreachable). • A routing or configuration issue led to no timely provisional responses (100/180), causing your side to cancel the INVITE. • Network transport quirks (packet loss, NAT timeouts) causing retry/cancel behavior. Suggested next diagnostic steps 1. Correlate timing: check upstream logs (your SIP stack/SIP trunk provider) for the moment the CANCEL (if any) originated and who sent it. 2. Check SIP traces (full INVITE/CANCEL/ACK exchange) between LiveKit and the trunk to see which side initiated the CANCEL and why. 3. Verify trunk/provider status and any rate limits or blocking on the destination number (+917478886371). 4. Confirm SIP timers and timeouts in your LiveKit SIP configuration — if your system times out early it may send a CANCEL. 5. Look for provider-side reasons (Twilio’s logs) for quick call termination or rejection. 6. If this is reproducible, capture a pcap or detailed SIP logs showing the full message exchange for the failed callID (SCL_t2m3WHeSY95H) and traceID (74326d3357486553). If you’d like, I can: • Help you interpret a full SIP trace if you paste it here. • Suggest specific config values to adjust timeouts in LiveKit SIP if you confirm which settings you currently use. Which would you prefer? Sources: sip | livekit/livekit_composite | DeepWiki
l
2026-03-05T210704.838Z WARN livekit.psrpc.SIPInternal.CreateSIPParticipant rpc/logging.go:66 client error {"topic": [""], "request": {"sipCallId": "SCL_et9kVwJ5ekKC", "sipTrunkId": "ST_NjwdW22CmFhc", "address": "nsg.pstn.twilio.com", "transport": "SIP_TRANSPORT_AUTO", "number": "+", "callTo": "+917478886371", "username": "<redacted (9 bytes)>", "password": "<redacted (20 bytes)>", "roomName": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "participantIdentity": "sip-participant", "participantMetadata": "<redacted (17 bytes)>", "participantAttributes": "<redacted (167 bytes)>", "includeHeaders": "SIP_NO_HEADERS", "enabledFeatures": ["KRISP_ENABLED"], "mediaEncryption": "SIP_MEDIA_ENCRYPT_DISABLE", "waitUntilAnswered": true}, "response": null, "duration": "688.024132ms", "error": "unexpected status from INVITE response: sip status: 403: Caller ID is unauthorized. A Valid Caller ID is either a DID you have purchased from Twilio or a verified Caller ID."} 2026-03-05T210704.838Z ERROR livekit service/sip.go:596 cannot create sip participant {"room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "sipTrunk": "ST_NjwdW22CmFhc", "toUser": "+917478886371", "participant": "sip-participant", "callID": "SCL_et9kVwJ5ekKC", "fromUser": "+", "toHost": "nsg.pstn.twilio.com", "error": "unexpected status from INVITE response: sip status: 403: Caller ID is unauthorized. A Valid Caller ID is either a DID you have purchased from Twilio or a verified Caller ID."} github.com/livekit/livekit-server/pkg/service.(*SIPService).CreateSIPParticipant /workspace/pkg/service/sip.go:596 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON.func1.1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4380 github.com/livekit/protocol/utils/xtwirp.DefaultServerOptions.ServerPassErrorDetails.func2.1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/utils/xtwirp/errors.go:165 github.com/livekit/protocol/utils/xtwirp.DefaultServerOptions.ServerPassTimeout.func1.1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/utils/xtwirp/timeout.go:50 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON.func1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4382 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON.func2 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4398 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4399 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipant /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4339 github.com/livekit/protocol/livekit.(*sIPServer).ServeHTTP /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:1799 github.com/livekit/protocol/utils/xtwirp.PassHeadersHandler.func1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/utils/xtwirp/headers.go:36 net/http.HandlerFunc.ServeHTTP /usr/local/go/src/net/http/server.go:2322 net/http.(*ServeMux).ServeHTTP /usr/local/go/src/net/http/server.go:2861 github.com/urfave/negroni/v3.(*Negroni).UseHandler.Wrap.func1 /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:59 github.com/urfave/negroni/v3.HandlerFunc.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:33 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 net/http.HandlerFunc.ServeHTTP /usr/local/go/src/net/http/server.go:2322 github.com/livekit/livekit-server/pkg/service.(*APIKeyAuthMiddleware).ServeHTTP /workspace/pkg/service/auth.go:107 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/livekit/livekit-server/pkg/service.RemoveDoubleSlashes /workspace/pkg/service/utils.go:76 github.com/urfave/negroni/v3.HandlerFunc.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:33 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/rs/cors.(*Cors).ServeHTTP /go/pkg/mod/github.com/rs/cors@v1.11.1/cors.go:324 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/urfave/negroni/v3.(*Recovery).ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/recovery.go:210 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/urfave/negroni/v3.(*Negroni).ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:111 net/http.serverHandler.ServeHTTP /usr/local/go/src/net/http/server.go:3340 net/http.(*conn).serve /usr/local/go/src/net/http/server.go:2109 2026-03-05T210704.838Z INFO livekit.api service/twirp.go:145 API SIP.CreateSIPParticipant {"service": "SIP", "method": "CreateSIPParticipant", "room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "participant": "sip-participant", "toUser": "+917478886371", "trunkID": "ST_NjwdW22CmFhc", "callID": "SCL_et9kVwJ5ekKC", "fromUser": "+", "toHost": "nsg.pstn.twilio.com", "duration": "688.464995ms", "status": "403", "error": "twirp error unknown: unexpected status from INVITE response: sip status: 403: Caller ID is unauthorized. A Valid Caller ID is either a DID you have purchased from Twilio or a verified Caller ID.", "code": "permission_denied"} 2026-03-05T210705.116Z INFO livekit agent/client.go:176 failed to send job request {"error": "no response from servers", "namespace": "", "jobType": "JT_ROOM", "agentName": ""} 2026-03-05T210706.045Z INFO livekit service/roommanager.go:413 starting RTC session {"room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "roomID": "RM_WZQCyma8NzcH", "participant": "agent-AJ_txBizQBoSCGK", "pID": "PA_tYNABbVepNr2", "remote": false, "room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "nodeID": "ND_v2d4cSXBZLPw", "numParticipants": 0, "participantInit": {"Identity": "agent-AJ_txBizQBoSCGK", "Reconnect": false, "ReconnectReason": "RR_UNKNOWN", "AutoSubscribe": true, "AutoSubscribeDataTrack": "not-set", "Client": {"sdk": "NODE", "version": "0.13.23", "protocol": 16}, "Grants": {"Identity": "agent-AJ_txBizQBoSCGK", "Kind": "agent", "KindDetails": [], "Video": {"RoomCreate": false, "RoomList": false, "RoomRecord": false, "RoomAdmin": false, "RoomJoin": true, "Room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "CanPublish": true, "CanSubscribe": true, "CanPublishData": true, "CanPublishSources": [], "CanUpdateOwnMetadata": true, "IngressAdmin": false, "Hidden": false, "Recorder": false, "Agent": true, "CanSubscribeMetrics": false, "DestinationRoom": ""}, "SIP": {}, "Agent": {}, "Inference": {}, "Observability": {}, "RoomConfig": {}, "RoomPreset": ""}, "Region": "", "AdaptiveStream": false, "ID": "", "SubscriberAllowPause": "not-set", "DisableICELite": false, "CreateRoom": {"name": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101"}, "AddTrackRequests": [], "PublisherOffer": {}, "SyncState": {}, "UseSinglePeerConnection": false}} 2026-03-05T210706.047Z INFO livekit service/roommanager.go:1006 created TURN password {"username": "yInTwVmViJUQOlFdfKIo4bMkEiGlmTr40zmcwJJoCC", "password": "jNrTcfJMYCUKpyXKtBJ0IncWxDQ2ueVMTc7ZVCM2tLeB"} 2026-03-05T210707.114Z INFO livekit rtc/room.go:1259 participant active {"room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "roomID": "RM_WZQCyma8NzcH", "participant": "agent-AJ_txBizQBoSCGK", "pID": "PA_tYNABbVepNr2", "remote": false, "subscriberCandidates": ["[local][selected:1][trickle] udp4 host 83.228.212.27:55736 (resolved: 83.228.212.27:55736)", "[local][trickle] tcp4 host 83.228.212.27:7881 (resolved: 83.228.212.27:7881)", "[local][trickle] udp4 host 172.17.0.1:57928 (resolved: 172.17.0.1:57928)", "[local][trickle] tcp4 host 172.17.0.1:7881 (resolved: 172.17.0.1:7881)", "[remote][trickle] udp4 host 172.18.0.5:39329", "[remote][trickle] udp4 srflx 128.65.199....:39329 related 172.18.0.5:39329", "[remote][selected:1] udp4 prflx 128.65.199....:39329"], "connectionType": "udp", "clientInfo": {"sdk": "NODE", "version": "0.13.23", "protocol": 16}, "connectTime": "1.068471676s"} 2026-03-05T210709.115Z INFO livekit agent/client.go:148 not dispatching agent job since no worker is available {"agentName": "anirban", "jobType": "JT_PUBLISHER", "room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "roomID": "RM_WZQCyma8NzcH"} 2026-03-05T210709.115Z INFO livekit agent/client.go:148 not dispatching agent job since no worker is available {"agentName": "anirban", "jobType": "JT_PARTICIPANT", "room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "roomID": "RM_WZQCyma8NzcH"} 2026-03-05T210724.396Z INFO livekit.room rtc/room.go:807 closing room {"room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "roomID": "RM_WZQCyma8NzcH"} 2026-03-05T210724.396Z INFO livekit rtc/participant.go:1367 participant closing {"room": "outbound-5cf5028e-bcce-4a95-8b9f-4125a9837032-1772744824101", "roomID": "RM_WZQCyma8NzcH", "participant": "agent-AJ_txBizQBoSCGK", "pID": "PA_tYNABbVepNr2", "remote": false, "sendLeave": true, "reason": "ROOM_CLOSED", "isExpectedToResume": false} 2026-03-05T210724.396 logs from livekit server . any diagnosis
t
Thanks — I’m responding to the LiveKit server logs you pasted. I can see the core problem and will lay out a clear diagnosis plus concrete next steps to get outbound SIP calls working. Key finding (root cause) • The INVITE returned a SIP 403: "Caller ID is unauthorized. A Valid Caller ID is either a DID you have purchased from Twilio or a verified Caller ID." This is the immediate reason the CreateSIPParticipant call failed and the participant/call was not created. What the logs show (concise breakdown) • CreateSIPParticipant returned a client error with status 403 and the unauthorized caller ID message — LiveKit attempted an outbound INVITE to the Twilio PSTN gateway and Twilio rejected the caller ID. • Shortly after, the system logs the call as ended (invite-failed). You also see CANCEL/487 messages consistent with the INVITE being terminated after the rejection. • Additional logs show the room/agent lifecycle continuing (RTC session started, participant created locally), but the SIP leg never established because Twilio refused the INVITE. Why this happens • Twilio requires outbound caller IDs to be either: ◦ A DID (phone number) you purchased from Twilio and have configured on the trunk, or ◦ A phone number you have verified with Twilio as an allowed Caller ID. • If your From/CID is set to "+" or an unverified number, Twilio will reject the INVITE with 403. Concrete next steps to fix 1. Confirm the From/Caller ID being sent ◦ Inspect the SIP INVITE from LiveKit (or your CreateSIPParticipant request) and confirm the
From
header value. The logs showed
"fromUser": "+"
which is not a valid configured caller ID. 1. Configure a valid caller ID in your trunk settings ◦ Use a Twilio DID assigned to your account and configured on the SIP trunk, or ◦ Verify the outbound number in Twilio (Verified Caller ID) and ensure the trunk allows that verified number. 1. Update LiveKit SIP participant creation payload ◦ When calling SIP.CreateSIPParticipant, set
fromUser
(caller ID) to the valid DID or verified caller ID configured in Twilio. Example: "+1XXXXXXXXXX". 1. Re-run and capture a SIP trace ◦ After updating the From header, reproduce the call and capture the INVITE/response exchange (full SIP trace) to confirm Twilio accepts the INVITE (you should see 100/180/200 as appropriate rather than 403). 1. Verify Twilio trunk routing and permissions ◦ Ensure the SIP trunk
ST_NjwdW22CmFhc
is configured to accept outbound calls to the destination region and that your account is not blocked or rate-limited. 1. Optional: add validation / guard rails ◦ Add startup checks in your app to assert
fromUser
is a configured DID to avoid runtime 403s. ◦ Log full
From
header and trunk id in the SIP request path so future failures are easier to trace. What to check in parallel • Twilio Console logs for the rejected call — Twilio will often include reason details and the exact caller ID it rejected. • Any NAT/transport differences if you see other call failures, but for this case the 403 is authoritative: it’s a caller-ID policy rejection at the provider. If you want, I can: • Show exactly where to set
fromUser
in your CreateSIPParticipant payload (paste the snippet you use to call the API), or • Help interpret a pasted SIP INVITE/response exchange (full trace) to confirm headers and the side that sent CANCEL. Which would you like me to do next?
l
const sipParticipant = await this.sipClient.createSipParticipant( trunk.trunkId, toNumber, roomName, { participantMetadata: JSON.stringify({ agent_id: '', }), fromNumber: '', waitUntilAnswered: true, krispEnabled: true, }, );
t
Thanks — I’m responding to your recent snippet that calls
createSipParticipant
and the logs showing the 403 from Twilio. What’s happening • Twilio rejected the outbound INVITE with 403: "Caller ID is unauthorized." The logs and your earlier output show
fromUser
/
fromNumber
was empty or set to
"+"
, which is not a valid caller ID for Twilio. Twilio requires the From/Caller-ID to be either a DID you purchased on Twilio or a phone number you’ve verified with them. What you should change • Supply a valid caller ID when creating the SIP participant. Use a Twilio DID assigned to your account (preferred) or a verified outbound caller ID that Twilio recognizes. Example change • Replace the empty
fromNumber
with a valid number string:
Copy code
const sipParticipant = await this.sipClient.createSipParticipant(
  trunk.trunkId,
  toNumber,
  roomName,
  {
    participantMetadata: JSON.stringify({ agent_id: '' }),
    fromNumber: '+1XXXXXXXXXX', // <-- set to your Twilio DID or verified number
    waitUntilAnswered: true,
    krispEnabled: true,
  },
);
Quick checklist to verify • Confirm the number you put in
fromNumber
is: ◦ A Twilio DID purchased on the same account as the trunk, or ◦ A caller ID you’ve verified in the Twilio console. • Check Twilio Console for the rejected call entry — it will show the exact From header Twilio received and confirm the reason. • Ensure the trunk configuration allows outbound calls from that DID/verified caller ID. If you want, I can: • Show how to read the exact From header from a SIP INVITE trace if you paste it. • Provide a short snippet that validates
fromNumber
exists and throws a clear error before sending the CreateSIPParticipant request. Which would you like next?
l
so basically twilio is rejecting the outbound call request because it cant see fromNumber
t
You’re right — Twilio is rejecting the outbound INVITE because the caller ID (
fromNumber
/
From
header) is missing or invalid. That causes the 403: "Caller ID is unauthorized." I’ll summarize what’s happening and give clear steps to fix it plus a small code validation you can add. Why this is happening • Twilio requires the outbound Caller ID to be either: ◦ A Twilio DID (a phone number you purchased on the same Twilio account), or ◦ A phone number you verified as an outbound caller ID in the Twilio Console. • When LiveKit sends an INVITE with an empty or invalid
From
(your logs showed
"fromUser": "+"
), Twilio rejects the INVITE with 403 and the SIP leg never connects. How to fix it (steps) 1. Pick a valid caller ID ◦ Preferred: Use a Twilio DID purchased on the same account as your SIP trunk. ◦ Alternative: Use a phone number you’ve verified in Twilio as an allowed outbound caller ID. 1. Set the caller ID in your createSipParticipant call ◦ Populate
fromNumber
with the valid E.164 phone number string (for example,
+14155551212
). Example change
Copy code
const sipParticipant = await this.sipClient.createSipParticipant(
  trunk.trunkId,
  toNumber,
  roomName,
  {
    participantMetadata: JSON.stringify({ agent_id: '' }),
    fromNumber: '+1XXXXXXXXXX', // <-- Twilio DID or verified number
    waitUntilAnswered: true,
    krispEnabled: true,
  },
);
1. Verify Twilio trunk and caller ID config ◦ Confirm the DID is attached to the SIP trunk
ST_NjwdW22CmFhc
or permitted for outbound on that trunk. ◦ If using a verified number, ensure the trunk/account policy allows that verified Caller ID. 1. Reproduce and inspect logs ◦ After updating
fromNumber
, retry. You should see Twilio respond with provisional responses (100 / 180) or 200 rather than 403. ◦ Check Twilio Console for the rejected call entry — it shows the
From
value Twilio received and any additional rejection details. Optional: Add a preflight validation in your code • Validate
fromNumber
before calling createSipParticipant and fail fast with a helpful message:
Copy code
if (!fromNumber || fromNumber === '+') {
  throw new Error('Invalid fromNumber: set a Twilio DID or verified caller ID before creating SIP participant');
}
If you want, I can: • Show exactly how to read the full SIP INVITE if you paste a SIP trace, to confirm the
From
header. • Provide a snippet that programmatically checks Twilio Console (or your config) for the DID before creating the SIP participant. • Help craft a small startup assertion to prevent misconfiguration. Which of these would be most helpful right now?
l
2026-03-05T214316.472Z INFO livekit agent/client.go:148 not dispatching agent job since no worker is available {"agentName": "anirban", "jobType": "JT_PUBLISHER", "room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "roomID": "RM_VffQMXxAmH2J"} 2026-03-05T214323.022Z WARN livekit.psrpc.SIPInternal.CreateSIPParticipant rpc/logging.go:66 client error {"topic": [""], "request": {"sipCallId": "SCL_WpqCbgDnW5ZH", "sipTrunkId": "ST_NjwdW22CmFhc", "address": "nsg.pstn.twilio.com", "transport": "SIP_TRANSPORT_AUTO", "number": "+18153489346", "callTo": "+917478886371", "username": "<redacted (9 bytes)>", "password": "<redacted (20 bytes)>", "roomName": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "participantIdentity": "sip-participant", "participantMetadata": "<redacted (17 bytes)>", "participantAttributes": "<redacted (178 bytes)>", "includeHeaders": "SIP_NO_HEADERS", "enabledFeatures": ["KRISP_ENABLED"], "mediaEncryption": "SIP_MEDIA_ENCRYPT_DISABLE", "waitUntilAnswered": true}, "response": null, "duration": "15.002463589s", "error": "request timed out"} 2026-03-05T214323.022Z ERROR livekit service/sip.go:596 cannot create sip participant {"room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "sipTrunk": "ST_NjwdW22CmFhc", "toUser": "+917478886371", "participant": "sip-participant", "callID": "SCL_WpqCbgDnW5ZH", "fromUser": "+18153489346", "toHost": "nsg.pstn.twilio.com", "error": "request timed out"} github.com/livekit/livekit-server/pkg/service.(*SIPService).CreateSIPParticipant /workspace/pkg/service/sip.go:596 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON.func1.1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4380 github.com/livekit/protocol/utils/xtwirp.DefaultServerOptions.ServerPassErrorDetails.func2.1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/utils/xtwirp/errors.go:165 github.com/livekit/protocol/utils/xtwirp.DefaultServerOptions.ServerPassTimeout.func1.1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/utils/xtwirp/timeout.go:50 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON.func1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4382 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON.func2 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4398 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipantJSON /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4399 github.com/livekit/protocol/livekit.(*sIPServer).serveCreateSIPParticipant /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:4339 github.com/livekit/protocol/livekit.(*sIPServer).ServeHTTP /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/livekit/livekit_sip.twirp.go:1799 github.com/livekit/protocol/utils/xtwirp.PassHeadersHandler.func1 /go/pkg/mod/github.com/livekit/protocol@v1.43.5-0.20260114074149-a8bb8204ce69/utils/xtwirp/headers.go:36 net/http.HandlerFunc.ServeHTTP /usr/local/go/src/net/http/server.go:2322 net/http.(*ServeMux).ServeHTTP /usr/local/go/src/net/http/server.go:2861 github.com/urfave/negroni/v3.(*Negroni).UseHandler.Wrap.func1 /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:59 github.com/urfave/negroni/v3.HandlerFunc.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:33 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 net/http.HandlerFunc.ServeHTTP /usr/local/go/src/net/http/server.go:2322 github.com/livekit/livekit-server/pkg/service.(*APIKeyAuthMiddleware).ServeHTTP /workspace/pkg/service/auth.go:107 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/livekit/livekit-server/pkg/service.RemoveDoubleSlashes /workspace/pkg/service/utils.go:76 github.com/urfave/negroni/v3.HandlerFunc.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:33 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/rs/cors.(*Cors).ServeHTTP /go/pkg/mod/github.com/rs/cors@v1.11.1/cors.go:324 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/urfave/negroni/v3.(*Recovery).ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/recovery.go:210 github.com/urfave/negroni/v3.middleware.ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:51 github.com/urfave/negroni/v3.(*Negroni).ServeHTTP /go/pkg/mod/github.com/urfave/negroni/v3@v3.1.1/negroni.go:111 net/http.serverHandler.ServeHTTP /usr/local/go/src/net/http/server.go:3340 net/http.(*conn).serve /usr/local/go/src/net/http/server.go:2109 2026-03-05T214323.022Z INFO livekit.api service/twirp.go:145 API SIP.CreateSIPParticipant {"service": "SIP", "method": "CreateSIPParticipant", "room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "participant": "sip-participant", "toUser": "+917478886371", "trunkID": "ST_NjwdW22CmFhc", "callID": "SCL_WpqCbgDnW5ZH", "fromUser": "+18153489346", "toHost": "nsg.pstn.twilio.com", "duration": "15.002984775s", "status": "408", "error": "twirp error unknown: request timed out", "code": "deadline_exceeded"} 2026-03-05T214323.025Z WARN livekit.transport rtc/transport.go:919 error reading data channel {"room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "roomID": "RM_VffQMXxAmH2J", "participant": "sip-participant", "pID": "PA_uA5p2mBXKkdJ", "remote": false, "transport": "PUBLISHER", "label": "_lossy", "error": "[0xc000f96848] abort chunk, with following errors: (User Initiated Abort: )"} 2026-03-05T214323.025Z WARN livekit.transport rtc/transport.go:919 error reading data channel {"room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "roomID": "RM_VffQMXxAmH2J", "participant": "sip-participant", "pID": "PA_uA5p2mBXKkdJ", "remote": false, "transport": "PUBLISHER", "label": "_reliable", "error": "[0xc000f96848] abort chunk, with following errors: (User Initiated Abort: )"} 2026-03-05T214323.025Z INFO livekit rtc/participant.go:1367 participant closing {"room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "roomID": "RM_VffQMXxAmH2J", "participant": "sip-participant", "pID": "PA_uA5p2mBXKkdJ", "remote": false, "sendLeave": true, "reason": "CLIENT_REQUEST_LEAVE", "isExpectedToResume": false} 2026-03-05T214323.026Z WARN livekit.transport rtc/transport.go:1276 error reading data channel {"room": "outbound-05f8c767-75fd-46e3-9725-e06e2cfd3b0f-1772746987987", "roomID": "RM_VffQMXxAmH2J", "participant": "sip-participant", "pID": "PA_uA5p2mBXKkdJ", "remote": false, "transport": "SUBSCRIBER", "label": "_reliable", "error": "[0xc000f96008] abort chunk, with following errors: (User Initiated Abort: )"} what is the issue now
t
Thanks — I’m responding to the latest logs you pasted. They show a different failure now: the SIP participant creation is timing out rather than being explicitly rejected. Quick summary of what’s happening now • Earlier you saw 403 (unauthorized Caller ID). After you set a valid From/caller ID, the CreateSIPParticipant request now times out (duration ~15s) and LiveKit reports a 408 / "request timed out". • The room and local agent RTC participant are created successfully, but the SIP leg never completes and the SIP participant/client leaves shortly after (CLIENT_REQUEST_LEAVE). Data channel warnings (abort chunk / User Initiated Abort) reflect the SIP leg ending. Likely causes to investigate • SIP trunk / provider didn’t respond to the INVITE within LiveKit’s timeout window (network connectivity, routing, or provider-side delays). • Transport issues between LiveKit and Twilio gateway (UDP/TCP/ TLS) or packet loss causing no response. • Authentication or trunk configuration on Twilio blocking the INVITE silently or not replying (e.g., credential mismatch, IP ACL, or trunk restrictions). • Intermediate NAT/firewall blocking SIP or media ports causing the session to fail to proceed. • No available worker/agent to handle the agent job (log: "not dispatching agent job since no worker is available") — this means the agent-side job to publish media or handle the SIP leg may not have been assigned, contributing to the timeout. Concrete next steps (ordered) 1. Confirm INVITE reached Twilio and check Twilio Console ◦ Check Twilio SIP trunk logs for the call attempt and any inbound INVITE or error. Twilio’s logs will show whether they received the INVITE and why they didn’t respond. 1. Capture SIP traces ◦ Capture the full SIP exchange (pcap or full SIP text) between LiveKit and nsg.pstn.twilio.com for the failing callID (SCL_WpqCbgDnW5ZH). Look for whether the INVITE was sent and whether any provisional responses (100/180) or 4xx/5xx responses came back. 1. Verify transport/protocol and credentials ◦ Ensure the transport LiveKit chose (UDP/TCP/TLS) is allowed by the Twilio gateway and your trunk config. ◦ Reconfirm username/password used for the trunk are correct and not being rejected silently. 1. Check network connectivity and firewall/NAT rules ◦ Ensure LiveKit node IPs can reach Twilio on SIP port 5060/5061 and that return packets are not blocked. ◦ If using UDP, consider testing with TCP/TLS to reduce NAT-related loss. 1. Re-check agent/worker availability ◦ The log "not dispatching agent job since no worker is available" suggests no worker handled the JT_PUBLISHER / JT_PARTICIPANT job. Confirm your agent workers are healthy and available so media can be published once the call is answered. 1. Increase or tune timeouts (temporary for debugging) ◦ If provider responses are slow, increase the SIP INVITE timeout in LiveKit or on your side temporarily to see whether slowing responses eventually arrive (don’t leave elevated timeouts in production without reason). 1. Reproduce with verbose logging ◦ Enable debug SIP logs on LiveKit and, if possible, on Twilio side. Reproduce a failed call and collect both sides’ logs to correlate timestamps. What to provide if you want me to dig deeper • The full SIP INVITE/response exchange (paste the SIP trace or pcap) for callID SCL_WpqCbgDnW5ZH. • The createSipParticipant request you used (you already shared snippet; include the exact fromNumber used). • Twilio SIP trunk settings (transport protocol allowed, authentication method, ACLs) — redacting secrets is fine. • Agent worker status / number of available workers when the call was attempted. If you want, I can: • Walk through how to capture a SIP trace and what lines to look for. • Help you craft a short validation to ensure workers are available before making the SIP request. • Suggest LiveKit config keys to increase timeout for testing. Which of the above would be most helpful next?
l
so if there is a timeout issue can we not icerese it
t
You’re asking whether you can increase the timeout to avoid these SIP outbound call timeouts. Short answer: LiveKit does not expose a single SIP signaling timeout you can directly raise, but you can and should take a few practical steps to reduce timeouts and make outbound calls more reliable. What I see right now • Earlier calls failed with Twilio 403 because From/caller ID was missing or invalid. • After fixing caller ID, the CreateSIPParticipant call is now timing out (≈15s) and the SIP leg never establishes. What you can do (concrete, ordered actions) 1. Ensure caller ID is valid (already done) • Use a Twilio DID owned by the same account or a verified caller ID in the Twilio Console. This prevents immediate 403 rejections. 1. Confirm the INVITE reached Twilio and check Twilio logs • Inspect Twilio SIP trunk logs for the callID to see whether Twilio received the INVITE and whether it responded. If Twilio never sees the INVITE, the timeout is a networking/transport problem. 1. Capture a SIP trace (recommended) • Capture the full INVITE/response exchange (pcap or SIP text) between your LiveKit node and nsg.pstn.twilio.com for the failing callID. This shows whether a response was sent and which side initiated any CANCEL. 1. Verify transport, credentials, and trunk settings • Ensure the transport (UDP/TCP/TLS) you use is allowed by Twilio and matches trunk config. • Reconfirm username/password and any IP ACLs for the trunk. • Confirm the trunk is allowed to place calls to the destination region/number. 1. Check network/NAT/firewall • Ensure LiveKit node IPs can reach Twilio’s SIP ports (5060/5061) and return packets can reach your node. • If using UDP and experiencing packet loss, try TCP/TLS to reduce transport-related timeouts. 1. Ensure agent workers are available • The log “not dispatching agent job since no worker is available” indicates no worker handled the JT_PUBLISHER/JT_PARTICIPANT job. Make sure your agent workers are up and healthy — otherwise media handshake or job dispatch may delay or fail and cause timeouts. 1. Retry/Backoff at application layer • Wrap createSipParticipant with retry and exponential backoff in your app. LiveKit doesn’t expose a direct INVITE timeout to increase, so retries help handle transient provider/network delays. Example simple retry pseudo-flow • Validate fromNumber (must be a Twilio DID/verified). • Try createSipParticipant. • On transient errors or timeout, wait (backoff) and retry N times before failing. 1. Temporarily increase debugging timeouts (for diagnosis) • If you can, temporarily increase any client-side request deadlines or LiveKit request wrappers while capturing logs to see if slow responses arrive. Don’t leave increased timeouts in production without reason. If you want, I can: • Provide a small retry wrapper snippet for your Node.js call to createSipParticipant. • Help interpret a SIP trace or Twilio log if you paste it here (redact secrets). • Recommend specific transport changes (UDP→TCP/TLS) and how to test them. Which would be most helpful right now — a retry snippet, help capturing/interpreting a SIP trace, or a checklist to validate Twilio trunk settings? Sources: SIP Lifecycle Management Agent | LiveKit Documentation | Outbound Calling Script | LiveKit Documentation | Agents Framework | livekit/livekit_composite | DeepWiki | Preserve caller ID during SIP transfers with Twilio | LiveKit | Agents telephony integration | LiveKit Documentation
l
twilio logs all SID CA2d7312f29477f6160ad476c6993cceb7 Date 031632 IST 2026-03-06 Start Time 031632 IST 2026-03-06 End Time 031639 IST 2026-03-06 Duration 0 sec Cost — From To +917478886371 Direction Trunking Terminating Type sip-pstn Trunk SID TK68c279d14b410d674b422e2214c65009 Status No Answer SIP PCAP Log Download Stir/Shaken Status —
t
Thanks — I’m responding to the Twilio call log you pasted (Call SID CA2d7312f29477f6160ad476c6993cceb7). That log confirms the SIP INVITE reached Twilio but the call ended with status "No Answer" after ~7 seconds. Below is a focused diagnosis and the next actions to resolve the timeout/no-answer outcome. What the Twilio log shows • Direction: Trunking → Terminating (LiveKit → PSTN via Twilio). • Status: No Answer and Duration 0s — Twilio accepted the INVITE but the destination did not answer or Twilio could not complete the PSTN leg. • There is a SIP PCAP available in Twilio you can download for the exact SIP exchange. What this implies • The SIP INVITE reached Twilio (so LiveKit→Twilio network likely OK). • Twilio attempted to complete the PSTN call to +917478886371 but either: ◦ The destination number did not answer, or ◦ Twilio could not complete the PSTN leg (carrier issues, number unreachable, or carrier-level blocking). • This is different from a 403 caller-ID rejection — that earlier error was likely fixed by providing a valid From. Now the problem is at the PSTN termination step. Concrete next steps (ordered, action-focused) 1) Download and inspect Twilio’s SIP PCAP • Download the SIP PCAP from the Twilio log link. Look for the INVITE Twilio sent to the PSTN gateway and any final SIP responses from carriers (e.g., 404, 486, 503) or a clear timeout. If you paste the sanitized SIP trace here I can help interpret it. 2) Confirm destination number reachability • Verify +917478886371 is callable from Twilio (try calling the number from the Twilio console or another PSTN line). • Check whether the destination blocks unknown or international calls, or if it uses carrier-level filtering. 3) Check Twilio call details for termination reason • In the Twilio Console call logs there may be additional metadata or error codes explaining "No Answer" (carrier-level cause or routing problem). 4) Correlate timestamps with LiveKit logs • Match Twilio timestamps with your LiveKit trace for the same Call SID/traceID to see whether LiveKit sent any CANCEL or timed out locally after Twilio returned no final answer. 5) Retry and add retries/backoff in your app • Implement 2–3 retries with exponential backoff around createSipParticipant for transient PSTN or carrier issues. That helps when carriers intermittently fail to complete termination. 6) Test alternate numbers and routes • Try calling a different PSTN number (local to the same country/region) to see if the problem is specific to that destination. • If you have multiple Twilio trunks or carriers, try routing via another trunk to isolate whether it’s carrier-specific. 7) If you see specific SIP error codes in the PCAP • 4xx/5xx codes will pinpoint cause (e.g., 404 Not Found, 486 Busy Here, 503 Service Unavailable). Share the relevant lines and I’ll interpret them. Quick checks you can run now • Reproduce one call while capturing LiveKit SIP debug logs and download the Twilio PCAP immediately. • From Twilio Console, open the Call SID and check any extra diagnostics or carrier error codes. Would you like me to: • Walk through which lines in the PCAP/SIP trace to paste here (sanitize auth/credentials) and then interpret them, or • Provide a small Node.js retry-wrapper for createSipParticipant you can drop in now? Tell me which and I’ll provide the exact next snippet or analysis steps.
l
so i believe there is no issue in teh livekit self hosted end and issue lies in twllio ?
t
You’re right to suspect the problem is now on the Twilio/PSTN side. The logs you shared show two distinct phases and conclusions: • Earlier: Twilio returned 403 "Caller ID is unauthorized" — that was a LiveKit-side configuration issue (missing/invalid From). You fixed that by supplying a valid caller ID. • Now: Twilio’s call log for the Call SID shows Status = "No Answer" and a short time window (INVITE reached Twilio, but PSTN leg never completed). That indicates LiveKit successfully sent the INVITE to Twilio, and the failure is happening during Twilio→PSTN termination (destination not answering, carrier routing, or provider-level blocking), not in your LiveKit server. Why I say the issue is on Twilio/PSTN now • Twilio’s logs show the call was Trunking→Terminating and include a downloadable SIP PCAP — meaning Twilio received the INVITE. • LiveKit timed out waiting for an answered call (request timed out / 408), while Twilio recorded “No Answer.” This matches Twilio attempting PSTN termination but not getting an answer or a final success response. Recommended next steps (practical and ordered) 1. Download and inspect Twilio’s SIP PCAP ◦ Look for Twilio→carrier SIP responses (e.g., 404, 486, 503) or clear timeouts. Paste sanitized relevant lines here and I’ll interpret them. 1. Check Twilio Console diagnostics for that Call SID ◦ Twilio sometimes includes carrier/termination error codes or notes that explain “No Answer.” 1. Test direct reachability ◦ Call +917478886371 from another PSTN line (or the Twilio Console test if available) to confirm the number answers and does not block unknown/international calls. 1. Try alternate destinations and trunks ◦ Place a call to another known-working PSTN number to see if termination succeeds. ◦ If you have another trunk or carrier, test routing via that trunk to isolate whether it’s carrier-specific. 1. Retry with backoff in your app ◦ Implement a few retries with exponential backoff around createSipParticipant to handle transient carrier failures. 1. Correlate timestamps and logs ◦ Match LiveKit and Twilio timestamps for the Call SID to confirm whether LiveKit sent any CANCEL or timed out locally before Twilio’s final state. If you want, I can help with either: • Interpreting specific lines from the Twilio PCAP (paste sanitized INVITE / final response lines), or • A small Node.js retry wrapper for createSipParticipant to reduce effect of transient PSTN failures. Which would you like next?
l
here is the pacp log as requested
t
Thanks — I’m ready to help decode the PCAP and pinpoint why Twilio shows “No Answer.” I didn’t receive the actual SIP messages yet. Please paste the relevant SIP exchange lines (sanitized) so I can interpret them. What to include (please redact credentials) • The initial INVITE that Twilio received or sent (full headers and SDP if present). • Any provisional responses (e.g., 100, 180) and the final response (e.g., 487, 486, 404, 503) from the carrier or PSTN. • Any CANCEL or BYE messages if present. • Timestamps and the Call SID (you already provided Call SID — good). How to sanitize • Replace or redact Authorization/Proxy-Authorization headers and any username/password values. • You can remove full SDP bodies if they contain private IPs — but include the m= lines and codecs if possible. • Only paste the SIP message text, not binary PCAP. If you prefer, instead of pasting raw SIP, you can: • Paste the Twilio PCAP SIP log download’s key message lines that show the INVITE and the terminal response, or • Copy the Twilio SIP log summary lines around the INVITE/termination. What I’ll do once you paste it • Identify whether Twilio forwarded the INVITE to carrier and what final SIP code the carrier returned. • Explain what that SIP response means (e.g., No Answer, Busy, Not Found, Network error). • Recommend exact next steps (provider/debugging actions) based on the SIP response (for example, retry, use alternative trunk, ask Twilio to check carrier route, or adjust LiveKit behavior). Paste the INVITE + final response (or the key SIP lines) and I’ll analyze them.
l
router.post( '/initiate', userMiddleware, orgRoleMiddleware(['Admin']), callLimiter, validate(initiateAgentCallSchema), async (req, res) => { try { const { agentId, trunkId, fromNumber, toNumber } = req.body; if (!trunkId || !fromNumber || !toNumber || !agentId) { return res.status(400).json({ success: false, message: 'Missing required fields: agentId, trunkId, fromNumber, toNumber', }); } const phoneRegex = /^\+\d{10,15}$/; if (!phoneRegex.test(toNumber)) { return res.status(400).json({ success: false, message: 'Invalid phone number format. Use E.164 format (e.g., +12135550100)', }); } const result = await livekitService.initiateCall(agentId, trunkId, fromNumber, toNumber); res.json({ success: true, message: result.message, data: result, }); return; } catch (error) { console.error('Failed to initiate call:', error); res.status(500).json({ success: false, message: 'Failed to initiate call', }); return; } }, ); 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}
); 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. Current status: ${agent.status}
); } const trunk = await prisma.outboundTrunk.findUnique({ where: { trunkId: trunkId, }, }); if (!trunk) { throw new Error('Trunk not found'); } if (!trunk?.numbers.includes(fromNumber)) { throw new Error(
Number ${fromNumber} not configured on trunk
); } const roomName = `outbound-${agentId}-${Date.now()}`; console.log(
Creating room: ${roomName}
); await this.roomClient.createRoom({ name: roomName, emptyTimeout: 300, maxParticipants: 10, }); console.log(
Room created: ${roomName}
); console.log(
Dispatching agent ${agent.name} to room ${roomName}
); const agentDispatch = await this.agentDispatchClient.createDispatch(roomName, agent.name, { metadata: JSON.stringify({ agent_id: agentId, trunk_id: trunkId, call_type: 'outbound', from_number: fromNumber, to_number: toNumber, }), }); console.log(
Agent dispatched: ${agentDispatch.id}
); console.log(
Creating SIP participant for ${toNumber}
); const sipParticipant = await this.sipClient.createSipParticipant( trunk.trunkId, toNumber, roomName, { participantMetadata: JSON.stringify({ agent_id: '', }), fromNumber: fromNumber, waitUntilAnswered: true, krispEnabled: true, }, ); console.log(
SIP participant created: ${sipParticipant.participantId}
); return { success: true, dispatch: agentDispatch, sipParticipant, roomName, 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; } } will retries resolve the isue
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
how would you implement retires with backoff around createSipParticipant