# Outbound call - waitForParticipant doesn't wait for the participant to answer the call

**URL:** <https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025>\
**Category:** Agents\
**Tags:** agent-sdk-node-js, node-js\
**Created:** [September 9, 2026, 9:30pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025 "2026-09-09T21:30:41Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![royibernthal](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/royibernthal/32/2510_2.png) [@royibernthal](https://community.livekit.io/u/royibernthal)\
**Post date:** [September 9, 2026, 9:30pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/1 "2026-09-09T21:30:41Z")

</div>

I’m using the Node SDK. I’m following this doc to make outbound calls:

> **[Make outbound calls | LiveKit Documentation](https://docs.livekit.io/telephony/making-calls/outbound-calls/)**
>
> Create a LiveKit SIP participant to make outbound calls.

In my backend, I’m using `createDispatch`:

```auto
const api = new LiveKitAPI({
  host: process.env.LIVEKIT_URL!,
  apiKey: process.env.LIVEKIT_API_KEY!,
  secret: process.env.LIVEKIT_API_SECRET!,
});

await api.agentDispatch.createDispatch(
  roomName,
  agentName,
  { metadata: JSON.stringify(metadata) },
);

```

In the LiveKit agent, I’m creating the SIP participant and waiting for it to join.

The problem is that `waitForParticipant` doesn’t wait for the participant to answer the call.

`"participant joined"` is logged immediately after `"waiting for participant"`, and the agent starts talking even though there’s no human on the call yet.

What am I missing?

I’ve also attached a small repro of the LiveKit agent. I assume a backend repro isn’t necessary since it’s just a single call to `createDispatch`.

```auto
import { ServerOptions, cli, defineAgent, log, voice } from '@livekit/agents';
import { LiveKitAPI } from 'livekit-server-sdk';
import * as openai from '@livekit/agents-plugin-openai';
import { audioEnhancement } from '@livekit/plugins-ai-coustics';
import dotenv from 'dotenv';
import { fileURLToPath } from 'node:url';
import { createAgent } from './agent.ts';

dotenv.config({ path: '.env.local' });

export default defineAgent({
  entry: async (ctx) => {
    const logger = log().child({ name: 'main' });

    await ctx.connect();

    const toNumber = JSON.parse(ctx.job.metadata).phoneNumber;

    logger.info({ toNumber }, 'creating outbound call');

    const api = new LiveKitAPI({
      host: process.env.LIVEKIT_URL!,
      apiKey: process.env.LIVEKIT_API_KEY!,
      secret: process.env.LIVEKIT_API_SECRET!,
    });

    try {
      await api.sip.createSipParticipant(
        process.env.LIVEKIT_SIP_OUTBOUND_TRUNK!,
        toNumber,
        ctx.room.name!,
        {
          participantIdentity: toNumber,
          waitUntilAnswered: true,
        },
      );

      logger.info({ toNumber }, 'waiting for participant');

      await ctx.waitForParticipant(toNumber);

      logger.info({ toNumber }, 'participant joined'); // logged immediately. doesn't wait for participant to answer the call
    } catch (error) {
      logger.error(error);

      ctx.shutdown();

      return;
    }

    const session = new voice.AgentSession({
      llm: new openai.realtime.RealtimeModel({
        model: 'gpt-realtime-2.1',
        voice: 'marin',
        inputAudioTranscription: null,
      }),
    });

    await session.start({
      agent: createAgent(),
      room: ctx.room,
      inputOptions: {
        noiseCancellation: audioEnhancement({ model: 'quailVfS' }),
      },
    });

    logger.info('generating reply');

    session.generateReply({
      instructions: 'Greet the user in a helpful and friendly manner.',
    });
  },
});

cli.runApp(
  new ServerOptions({
    agent: fileURLToPath(import.meta.url),
    agentName: process.env.LIVEKIT_AGENT_NAME!,
  }),
);

```

[livekit-outbound-test.zip](https://community.livekit.io/uploads/short-url/5SQgj8SKKbMUZX04ua8bgGbpCiI.zip) (57.8 KB)

---

<div class="post-metadata">

**Author:** ![areeb\_mohsin1](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/areeb_mohsin1/32/2438_2.png) [@areeb\_mohsin1](https://community.livekit.io/u/areeb_mohsin1)\
**Post date:** [September 9, 2026, 9:40pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/2 "2026-09-09T21:40:44Z")

</div>

`waitForParticipant` is working correctly, it’s just not the signal you want. The SIP participant is added to the room as soon as your provider accepts the INVITE, which is while it’s still ringing. The docs describe watching the `sip.callStatus` attribute from that point and waiting for it to turn `active`.

So drop `waitForParticipant` entirely. `waitUntilAnswered: true` is the right mechanism and should block until pickup, throwing `SipCallError` if the call fails or is rejected.

Which raises why it isn’t blocking for you. Your import is `'./agent.ts'` with the extension, so you’re on Node type stripping or a transpile-only runner. Neither type checks, so if your installed `livekit-server-sdk` doesn’t have `waitUntilAnswered` in `CreateSipParticipantOptions`, it gets silently dropped rather than erroring. Worth checking your installed version, and running `tsc --noEmit` once to see whether the option is valid against your types.

Easiest way to confirm is to time it:

```ts
const t = Date.now();
await api.sip.createSipParticipant(trunk, toNumber, ctx.room.name!, {
  participantIdentity: toNumber,
  waitUntilAnswered: true,
  ringingTimeout: 30,
});
logger.info({ ms: Date.now() - t }, 'createSipParticipant returned');

```

Answer the phone after a few rings. If that logs tens of milliseconds rather than the seconds you spent ringing, the option isn’t reaching the server.

As a belt-and-braces fix either way, gate `session.start` on the participant’s `sip.callStatus` attribute being `active` rather than on presence. The values are `dialing`, `ringing`, `active` and `hangup`, and it updates through attribute change events.

---

<div class="post-metadata">

**Author:** ![royibernthal](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/royibernthal/32/2510_2.png) [@royibernthal](https://community.livekit.io/u/royibernthal)\
**Post date:** [September 9, 2026, 10:31pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/3 "2026-09-09T22:31:00Z")

</div>

Got it, good to know.

`tsc --noEmit` ran without errors.

The log after `createSipParticipant` (“createSipParticipant returned”) was logged after 1573 ms - while the call was still ringing, or perhaps before it even started ringing.

`livekit-server-sdk` version is 2.18.0, which is the latest. I’m not entirely sure I understood why the option would be dropped.

Ideally I’d like to make it work with `waitUntilAnswered`, but I’ve also tried the workaround you suggested - `sip.callStatus` is immediately `active`, before the call is answered:

```auto
await api.sip.createSipParticipant(
  process.env.LIVEKIT_SIP_OUTBOUND_TRUNK!,
  toNumber,
  ctx.room.name!,
  {
    participantIdentity: toNumber,
    waitUntilAnswered: true,
    ringingTimeout: 30,
  },
);

const participant = await ctx.waitForParticipant(toNumber);

logger.info(participant.attributes, 'participant.attributes'); // sip.callStatus is immediately "active"

```

---

<div class="post-metadata">

**Author:** ![areeb\_mohsin1](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/areeb_mohsin1/32/2438_2.png) [@areeb\_mohsin1](https://community.livekit.io/u/areeb_mohsin1)\
**Post date:** [September 9, 2026, 11:03pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/4 "2026-09-09T23:03:07Z")

</div>

Thanks for testing it. And your `sip.callStatus` result narrows it usefully.

That attribute is server state rather than something the SDK derives. So both signals agree: the LiveKit server considered the call connected at about 1.5 seconds. The SDK waited and was told it had been answered. That moves the question upstream of your code, which is progress even though it doesn’t fix it yet.

Two explanations fit. Either your provider returned a `200 OK` before the human picked up, which is a false answer supervision problem on the carrier side, or the server marked the call active earlier than it should have. Worth noting that a carrier playing ringback as early media should be sending `183 Session Progress`, and that alone shouldn’t mark the call answered.

Your own detail decides a lot of it. You said the return may have come before the phone even started ringing. If that’s right, a carrier answer is a harder explanation to sustain, and it points more at the server side.

Fastest way to settle it is the SIP trace. Grab the `SCL_` call ID from your dashboard and ask which response came back at roughly 1.5 seconds. A `200 OK` makes it a carrier conversation about answer supervision. Anything else and it’s worth a LiveKit issue with that call ID attached.

Also worth logging the attribute repeatedly rather than once, so you can see whether it ever passes through `ringing` or goes straight to `active`.

---

<div class="post-metadata">

**Author:** ![royibernthal](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/royibernthal/32/2510_2.png) [@royibernthal](https://community.livekit.io/u/royibernthal)\
**Post date:** [September 9, 2026, 11:24pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/5 "2026-09-09T23:24:33Z")

</div>

Appreciate the detailed responses.

Perhaps it’s safe to assume the problem is in my code or LiveKit’s SDK / backend rather than with the carrier?

@darryncampbell @CWilson Thoughts?

---

<div class="post-metadata">

**Author:** ![darryncampbell](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/darryncampbell/32/20_2.png) [@darryncampbell](https://community.livekit.io/u/darryncampbell)\
**Post date:** [September 10, 2026, 9:22am UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/6 "2026-09-10T09:22:52Z")

</div>

I agree with @areeb_mohsin1 , understanding what happened at roughly 1.5 seconds would be key to understanding the root cause

---

<div class="post-metadata">

**Author:** ![royibernthal](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/royibernthal/32/2510_2.png) [@royibernthal](https://community.livekit.io/u/royibernthal)\
**Post date:** [September 10, 2026, 11:03am UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/7 "2026-09-10T11:03:02Z")

</div>

Fair enough.

Could you help clarify how I can tell which response came back at roughly 1.5 seconds after I grab the SCL\_ call ID? Are you referring to the call events?

> Fastest way to settle it is the SIP trace. Grab the `SCL_` call ID from your dashboard and ask which response came back at roughly 1.5 seconds.

 ![image](https://us1.discourse-cdn.com/flex001/uploads/livekit2/original/2X/7/71963f85f92ed92ef6ac759a6488d512ee26ff57.png)

---

<div class="post-metadata">

**Author:** ![darryncampbell](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/darryncampbell/32/20_2.png) [@darryncampbell](https://community.livekit.io/u/darryncampbell)\
**Post date:** [September 10, 2026, 1:26pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/8 "2026-09-10T13:26:38Z")

</div>

> [@areeb\_mohsin1](#):
>
> A `200 OK` makes it a carrier conversation about answer supervision. Anything else and it’s worth a LiveKit issue with that call ID attached.

I’m unsure why the timestamp for ‘Connected’ is identical to ‘call started’, but if you look at the PCAP you can see the `200 OK` message is arriving very quickly after the outbound call is established:

 ![Screenshot 2026-09-10 at 14.20.39](https://us1.discourse-cdn.com/flex001/uploads/livekit2/original/2X/c/c750e38a1e559a74d08d9b6db0b5ed3f2e77943d.png)

Which explains why you are seeing your `createSipParticipant` with `waitUntilAnswered` return quickly, since as far as LiveKit is concerned, the call was answered.

If you’re new to SIP, then we have an excellent primer for this in our docs:

> **[SIP handshake | LiveKit Documentation](https://docs.livekit.io/reference/telephony/sip-handshake/)**
>
> Understanding the SIP handshake process.

---

<div class="post-metadata">

**Author:** ![areeb\_mohsin1](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/areeb_mohsin1/32/2438_2.png) [@areeb\_mohsin1](https://community.livekit.io/u/areeb_mohsin1)\
**Post date:** [September 10, 2026, 9:35pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/9 "2026-09-10T21:35:05Z")

</div>

Thanks @darryncampbell, that settles it. Worth pulling out the exact number, since it’s the bit that makes this actionable with a carrier.

`180 Ringing` is at 0.546078 and `200 OK` at 0.780186. A 234 millisecond gap. Nobody picks up a phone in 234ms, so this is a false answer, and everything after it follows correctly. LiveKit saw a `200 OK`, treated the call as connected, and `waitUntilAnswered` returned. Nothing in your code or the SDK is misbehaving.

@royibernthal one thing worth ruling out before you take this anywhere: what number were you dialing? If the destination terminates inside your provider’s own network rather than going out to the PSTN, an instant answer can be normal and the trace wouldn’t tell you anything about real calls. Worth repeating against a mobile on an ordinary carrier.

If it reproduces there, it’s a carrier ticket rather than a LiveKit one. Give them the SIP call ID and the 234ms figure and ask about answer supervision on that route.

And if you end up stuck with a route that behaves this way, the workaround is to stop using SIP answer as your cue and wait for speech from the far end before greeting. You lose the proactive greeting, but it survives a carrier that answers early.

---

<div class="post-metadata">

**Author:** ![royibernthal](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/royibernthal/32/2510_2.png) [@royibernthal](https://community.livekit.io/u/royibernthal)\
**Post date:** [September 10, 2026, 11:21pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/10 "2026-09-10T23:21:52Z")

</div>

@darryncampbell Thanks. I went over the SIP handshake docs and I can make more sense of the PCAP now.

@areeb_mohsin1 Outbound goes through Twilio, which handles the SIP termination. This premature `200 OK` issue happens when calling Quo numbers. When calling an ordinary carrier as you suggested things work as expected.

Since the calls originate from Twilio and it seems Twilio isn’t the one sending the `200 OK`, doesn’t that mean they’re going out to the PSTN in both cases? LMK if I’m misunderstanding how this works.

Either way, you were right - the problem isn’t with the code. I was too quick to assume that earlier.

I’ll reach out to Quo’s support.

Unfortunately product-wise it’s very important for outbound calls to have a proactive greeting - it’s more than just a greeting in practice. Hopefully this premature `200 OK` isn’t too common among popular providers.

---

<div class="post-metadata">

**Author:** ![royibernthal](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/royibernthal/32/2510_2.png) [@royibernthal](https://community.livekit.io/u/royibernthal)\
**Post date:** [September 20, 2026, 3:59pm UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/11 "2026-09-20T15:59:23Z")

</div>

Update from Quo: they confirmed they send SIP `200 OK` before a human answers, so they can establish the external SIP leg and play hold music or run call flows while ringing team members internally. The actual pickup then happens inside Quo, with no additional SIP signal sent.

So `waitUntilAnswered` isn’t 100% reliable for detecting a human answer. I imagine this behavior is not unique to Quo.

---

<div class="post-metadata">

**Author:** ![darryncampbell](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/darryncampbell/32/20_2.png) [@darryncampbell](https://community.livekit.io/u/darryncampbell)\
**Post date:** [September 28, 2026, 9:09am UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/12 "2026-09-28T09:09:18Z")

</div>

> [@royibernthal](#):
>
> So `waitUntilAnswered` isn’t 100% reliable for detecting a human answer. I imagine this behavior is not unique to Quo.

👀 Interesting, thanks for sharing. I haven’t come across this before but I agree it’s possibly not unique.

---

<div class="post-metadata">

**Author:** ![system](https://sea1.discourse-cdn.com/flex001/user_avatar/community.livekit.io/system/32/26_2.png) [@system](https://community.livekit.io/u/system)\
**Post date:** [October 13, 2026, 9:10am UTC](https://community.livekit.io/t/outbound-call-waitforparticipant-doesnt-wait-for-the-participant-to-answer-the-call/2025/13 "2026-10-13T09:10:18Z")

</div>

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.
