LiveKit docs › Telephony › Features › Answering machine detection

---

# Answering machine detection

> Classify whether a real person, voicemail, or IVR system answered an outbound call.

## Overview

An outbound call can reach a person, voicemail, an IVR menu, or a number that can't accept messages. Answering machine detection (AMD) listens to the start of the call, classifies it with an LLM, and returns a result so your agent can respond appropriately.

## How AMD works

AMD runs once at the start of the call, on the first user utterance. It doesn't monitor continuously. While AMD is running, the agent's speech is paused so it doesn't talk over a voicemail greeting before classification completes.

AMD classifies the call into one of five categories. Your agent uses the result to decide the next step: continue the conversation, leave a voicemail, navigate an IVR, or hang up.

| Category | Description |
| `human` | A real person answered. Proceed with normal conversation. |
| `machine-ivr` | An IVR or DTMF menu was detected. In Python, the session automatically starts [IVR navigation](https://docs.livekit.io/telephony/features/dtmf.md) when `ivr_detection` is enabled (the default). The Node.js SDK doesn't support IVR navigation, so the agent should handle `machine-ivr` the same as `human` and let the main agent respond. |
| `machine-vm` | A voicemail greeting where leaving a message is possible. |
| `machine-unavailable` | The mailbox is full, not set up, or the callee is unreachable. Leaving a message isn't possible. |
| `uncertain` | The greeting can't be classified with confidence, or no speech is detected at all (for example, the callee answers but stays silent). Treat as a human and proceed with normal conversation. |

AMD runs two paths in parallel: a fast-path heuristic for short greetings followed by silence, and an LLM classifier for transcripts that need more reasoning. The first path to reach a conclusion produces the result.

![AMD classification flow: short speech and transcript inputs feed a fast-path heuristic and an LLM classifier, which together emit one of five categories: human, machine-ivr, machine-vm, machine-unavailable, or uncertain.](/images/sip/amd-pipeline.svg)

## Usage

Initialize AMD before creating the SIP participant so detection is ready before audio starts arriving. The detector pauses agent speech until a result is available.

**Python**:

Open the async context manager, then create the SIP participant inside it. Pass `participant_identity` so AMD's timers wait for that specific participant's audio track:

```python
import os

from livekit.agents import AMD
from livekit.protocol.sip import SIPOutboundConfig

async with AMD(session, participant_identity=participant_identity) as detector:
    await ctx.api.sip.create_sip_participant(
        api.CreateSIPParticipantRequest(
            trunk=SIPOutboundConfig(
                hostname=os.getenv("SIP_TRUNK_HOSTNAME"),
                auth_username=os.getenv("SIP_AUTH_USERNAME"),
                auth_password=os.getenv("SIP_AUTH_PASSWORD"),
            ),
            sip_number="<SIP provider number>",
            room_name=ctx.room.name,
            sip_call_to=phone_number,
            participant_identity=participant_identity,
            wait_until_answered=True,
        )
    )
    await ctx.wait_for_participant(identity=participant_identity)

    result = await detector.execute()

    if result.category == "human" or result.category == "uncertain":
        logger.info(
            "human answered the call or amd is uncertain, proceeding with normal conversation",
            extra={"transcript": result.transcript},
        )
    elif result.category == "machine-ivr":
        logger.info("ivr menu detected, starting navigation")
    elif result.category == "machine-vm":
        logger.info("voicemail detected, leaving a message")
        speech_handle = session.generate_reply(
            instructions=(
                "You've reached voicemail. Leave a brief message asking "
                "the customer to call back."
            ),
        )
        await speech_handle.wait_for_playout()
        ctx.shutdown("voicemail detected")
    elif result.category == "machine-unavailable":
        logger.info("mailbox unavailable, ending call")
        ctx.shutdown("mailbox unavailable")

```

---

**Node.js**:

Instantiate the detector before creating the SIP participant. Pass `participantIdentity` so AMD's timers wait for that participant's audio track. Wrap the run in `try`/`finally` so `detector.aclose()` runs even on error:

```typescript
import { voice } from '@livekit/agents';
import { LiveKitAPI } from 'livekit-server-sdk';
import { SIPOutboundConfig } from '@livekit/protocol';

session._roomIO.setParticipant(participantIdentity);
const detector = new voice.AMD(session, { participantIdentity });

try {
  const api = new LiveKitAPI();
  await api.sip.createSipParticipant(
    '', // Empty string when using inline trunk config
    phoneNumber,
    ctx.room.name,
    {
      participantIdentity,
      fromNumber: '<SIP provider number>',
      waitUntilAnswered: true,
    },
    new SIPOutboundConfig({ // Inline trunk configuration
      hostname: process.env.SIP_TRUNK_HOSTNAME,
      authUsername: process.env.SIP_AUTH_USERNAME,
      authPassword: process.env.SIP_AUTH_PASSWORD,
    }),
  );
  await ctx.waitForParticipant(participantIdentity);

  const result = await detector.execute();

  if (
    result.category === voice.AMDCategory.HUMAN ||
    result.category === voice.AMDCategory.UNCERTAIN ||
    result.category === voice.AMDCategory.MACHINE_IVR
  ) {
    logger.info(
      { amd: result },
      'human or ivr menu detected, proceeding with normal conversation',
    );
  } else if (result.category === voice.AMDCategory.MACHINE_VM) {
    logger.info({ amd: result }, 'voicemail detected, leaving a message');
    const speechHandle = session.generateReply({
      instructions:
        "You've reached voicemail. Leave a brief message asking the customer to call back.",
    });
    await speechHandle.waitForPlayout();
    session.shutdown({ reason: 'amd:machine-vm' });
  } else if (result.category === voice.AMDCategory.MACHINE_UNAVAILABLE) {
    logger.info({ amd: result }, 'mailbox unavailable, ending call');
    session.shutdown({ reason: 'amd:machine-unavailable' });
  }
} finally {
  await detector.aclose();
}

```

> ℹ️ **Stored outbound trunk**
> 
> You can also use a stored outbound trunk by passing `sip_trunk_id` (Python) or `sipTrunkId` (Node.js) instead of [inline trunk configuration](https://docs.livekit.io/telephony/making-calls/outbound-calls.md#inline-trunk). For details, see [Outbound trunk](https://docs.livekit.io/telephony/making-calls/outbound-trunk.md).

## Recommended models

AMD has been evaluated against a small set of LLMs and STT models on [LiveKit Inference](https://docs.livekit.io/agents/models/inference.md).

Behavior on unevaluated models isn't guaranteed, so AMD logs a compatibility warning when you pass an unevaluated model. Once you've validated your own choice, set `suppress_compatibility_warning=True` (Python) or `suppressCompatibilityWarning: true` (Node.js) to silence the warning.

### Evaluated LLMs

- `google/gemini-3.1-flash-lite` (default)
- `google/gemini-3-flash-preview`
- `google/gemini-2.5-flash-lite`
- `openai/gpt-4o`
- `openai/gpt-4.1`
- `openai/gpt-4.1-mini`
- `openai/gpt-4.1-nano`
- `openai/gpt-5.1`
- `openai/gpt-5.1-chat-latest`
- `openai/gpt-5.2`
- `openai/gpt-5.2-chat-latest`
- `openai/gpt-5.4`

### Evaluated STT models

- `cartesia/ink-whisper` (default)
- `assemblyai/universal-streaming-multilingual`
- `deepgram/nova-3`

## Parameters

Defaults are calibrated for typical outbound calls. Override them when you need different timing thresholds or a different classification prompt.

**Python**:

- **`llm`** _(LLM | str)_ (optional): LLM used for greeting classification. Accepts an `LLM` instance or a [LiveKit Inference](https://docs.livekit.io/agents/models/llm.md) model ID string. If not set, AMD uses `google/gemini-3.1-flash-lite` via LiveKit Inference when available, and otherwise falls back to the session's own LLM. See [recommended models](#models) for the evaluated set.

- **`stt`** _(STT | str)_ (optional): STT used to transcribe the greeting. Accepts an `STT` instance or a [LiveKit Inference](https://docs.livekit.io/agents/models/stt.md) model ID string. If not set, AMD uses `cartesia/ink-whisper` via LiveKit Inference when available, and otherwise reuses the session's existing STT transcripts. AMD runs its own STT pipeline so it can listen even when the session uses a realtime model with no separate STT.

- **`interrupt_on_machine`** _(bool)_ (optional) - Default: `True`: Interrupt any pending agent speech when a machine is detected.

- **`participant_identity`** _(str)_ (optional): Identity of the SIP participant whose audio AMD listens to. When omitted, AMD binds to the first remote participant that publishes an audio track. Set this in rooms with other participants so AMD listens to the caller instead of whichever track is subscribed first.

- **`ivr_detection`** _(bool)_ (optional) - Default: `True`: Automatically start [IVR navigation](https://docs.livekit.io/telephony/features/dtmf.md) when the result is `machine-ivr`. When `False`, AMD returns the `machine-ivr` result without starting navigation, and your agent decides how to handle it.

- **`detection_options`** _(DetectionOptions)_ (optional): Tune the detection timing and classification prompt. Pass a dict with any of these keys, with times in seconds:

- `human_speech_threshold` (default `2.5`): Maximum length of a "short greeting." Shorter speech takes the fast-path human heuristic; longer speech is treated as machine-like and defers to the LLM.
- `human_silence_threshold` (default `0.5`): Silence after a short greeting before AMD settles as `human`. Shorter values commit faster on quick "Hello?" greetings.
- `machine_silence_threshold` (default `1.5`): Silence after machine-like speech before AMD emits a verdict. Longer values give the LLM more time to review the transcript.
- `no_speech_threshold` (default `10.0`): Maximum time to wait for any speech before AMD settles as `uncertain`. The clock starts when the call is answered, so ringback and early media don't count against it.
- `timeout` (default `20.0`): Caps the entire detection, but only when `wait_until_finished` is `False`; with the default `True`, a long greeting can run past it (see `wait_until_finished`). The timer starts when AMD begins and resets once the participant's audio track is subscribed, so the effective ceiling can reach roughly twice this value.
- `max_endpointing_delay` (default `3.0`): How long AMD keeps waiting for the greeting to end while `wait_until_finished` is set, if the turn detector never signals the end. After this, AMD treats the turn as ended and classifies. Defaults to the session's endpointing delay, then `3.0`.
- `prompt`: Override the classification prompt passed to the LLM.
Unset keys use the defaults.

- **`wait_until_finished`** _(bool)_ (optional) - Default: `True`: Wait for the greeting to finish before classifying, so the agent doesn't talk over a voicemail. With the default `True`, once AMD hears speech it waits for post-speech silence and either a confirmed end of turn or the `max_endpointing_delay` before emitting, instead of forcing a result at `timeout`. A long greeting can therefore run past `timeout`. Set `False` to make `timeout` a hard cap regardless of speech. The `no_speech_threshold` still applies.

- **`suppress_compatibility_warning`** _(bool)_ (optional) - Default: `False`: Silence the warning that fires when `llm` or `stt` isn't among the evaluated models. Has no effect on classification behavior.

---

**Node.js**:

The Node.js SDK doesn't support IVR navigation, so treat `machine-ivr` results as a human conversation and let the main agent respond.

- **`llm`** _(LLM | string)_ (optional): LLM used for greeting classification. Accepts an `LLM` instance or a [LiveKit Inference](https://docs.livekit.io/agents/models/llm.md) model ID string. If not set, AMD uses `google/gemini-3.1-flash-lite` via LiveKit Inference when available, and otherwise falls back to the session's own LLM. See [recommended models](#models) for the evaluated set.

- **`stt`** _(STT | string)_ (optional): STT used to transcribe the greeting. Accepts an `STT` instance or a [LiveKit Inference](https://docs.livekit.io/agents/models/stt.md) model ID string. If not set, AMD uses `cartesia/ink-whisper` via LiveKit Inference when available, and otherwise listens to session-level transcripts instead. AMD runs its own STT pipeline so it can listen even when the session uses a realtime model with no separate STT.

- **`interruptOnMachine`** _(boolean)_ (optional) - Default: `true`: Interrupt any pending agent speech when a machine is detected.

- **`participantIdentity`** _(string)_ (optional): Identity of the SIP participant whose audio AMD should listen to. When omitted, AMD attaches to the session's linked participant or the first remote audio track in the room. Set this when the room might have other participants so AMD timers don't start on the wrong track.

- **`humanSpeechThresholdMs`** _(number)_ (optional) - Default: `2500`: Maximum length in milliseconds of a "short greeting." Shorter speech takes the fast-path human heuristic; longer speech is treated as machine-like and defers to the LLM.

- **`humanSilenceThresholdMs`** _(number)_ (optional) - Default: `500`: Silence in milliseconds after a short greeting before AMD settles as `human`. Shorter values commit faster on quick "Hello?" greetings.

- **`machineSilenceThresholdMs`** _(number)_ (optional) - Default: `1500`: Silence in milliseconds after machine-like speech before AMD emits a verdict. Longer values give the LLM more time to review the transcript.

- **`noSpeechTimeoutMs`** _(number)_ (optional) - Default: `10000`: Maximum time in milliseconds to wait for any speech before AMD settles as `uncertain`. The clock starts when the call is answered, so ringback and early media don't count against it.

- **`detectionTimeoutMs`** _(number)_ (optional) - Default: `20000`: Caps the entire detection, but only when `waitUntilFinished` is `false`; with the default `true`, a long greeting can run past it (see `waitUntilFinished`). The timer starts when AMD begins and resets once the participant's audio track is subscribed, so the effective ceiling can reach roughly twice this value.

- **`waitUntilFinished`** _(boolean)_ (optional) - Default: `true`: Wait for the greeting to finish before classifying, so the agent doesn't talk over a voicemail. With the default `true`, once speech is heard AMD waits for post-speech silence and either a confirmed end of turn or the `maxEndpointingDelayMs` before emitting, instead of forcing a result at `detectionTimeoutMs`. A long greeting can therefore run past `detectionTimeoutMs`. Set `false` to keep `detectionTimeoutMs` a hard cap. `noSpeechTimeoutMs` still applies.

- **`maxEndpointingDelayMs`** _(number)_ (optional): How long in milliseconds AMD keeps waiting for the greeting to end while `waitUntilFinished` is set, if the turn detector never signals the end. After this, AMD treats the turn as ended and classifies. Defaults to the session's endpointing delay, or `3000` when none is available.

- **`prompt`** _(string)_ (optional): Override the default classification prompt passed to the LLM. Use this to bias detection toward your domain (for example, recognizing region-specific voicemail phrasing) or to translate the prompt into another language.

- **`suppressCompatibilityWarning`** _(boolean)_ (optional) - Default: `false`: Silence the warning that fires when `llm` or `stt` isn't among the evaluated models. Has no effect on classification behavior.

## Additional resources

- **[AMD example (Python)](https://github.com/livekit/agents/blob/main/examples/telephony/amd.py)**: Outbound voice agent that runs AMD before responding and branches on the classification result.

- **[AMD example (Node.js)](https://github.com/livekit/agents-js/blob/main/examples/src/telephony_amd.ts)**: Outbound voice agent that runs AMD before responding and branches on the classification result.

- **[DTMF and IVR navigation](https://docs.livekit.io/telephony/features/dtmf.md)**: Send and receive DTMF tones, and navigate IVR systems after AMD detection.

- **[Outbound calls](https://docs.livekit.io/telephony/making-calls/outbound-calls.md)**: Create SIP participants and place outbound calls that AMD can classify.

---

This document was rendered at 2026-10-07T16:48:47.328Z.
For the latest version of this document, see [https://docs.livekit.io/telephony/features/answering-machine-detection.md](https://docs.livekit.io/telephony/features/answering-machine-detection.md).

To explore all LiveKit documentation, see [llms.txt](https://docs.livekit.io/llms.txt).