Detecting iOS 26 Call Screening and Leaving Voicemail​: ​How Twilio AMD and Real-Time Transcriptions Make It Possible

November 03, 2025
Written by
Reviewed by
Paul Kamp
Twilion

If you’ve been making outbound voice calls since Apple’s iOS 26 release, you’ve likely run into a new flow: call screening. Now, when you dial an iPhone with this feature enabled, you may not instantly reach your recipient; first, an automated voice asks you (or your app!) to “record your name and reason for calling”. If the person doesn’t pick up, you’re sent to voicemail. For brands trying to reach customers with high fidelity, this adds uncertainty:

  • Did a human answer or was it screening?
  • When should we leave a voicemail so our message isn't lost?
  • How do I avoid repeating myself or missing my chance?

You need a precise, automated way for your app to detect when it is speaking with the iOS call screen and either leave a message or reply with who is calling.

That’s where combining Twilio’s Answering Machine Detection and Real-Time Transcriptions shines. With a bit of code, you can now offer a human-grade experience, even in this complex new landscape – and today, we’ll share with you what that code might look like. Let’s dive in.

How does iOS 26 Call Screening work?

Let’s break it down – when a user has Screen Unknown Callers enabled in iOS 26, your inbound call will trigger the following flow:

  1. Screening Preamble: iOS 26 picks up the call and says something like: “Hi, if you record your name and reason for calling, I'll see if this person is available.”
  2. Your App’s Turn: You need to record your intent or identification (so a human can see it and decide to answer).
  3. Fork in the Road:
    • If answered by the person: conversation begins.
    • If rejected or ignored: goes to voicemail or the call is ended.
    • If screening is disabled: standard call/voicemail process.
iPhone screen showing call screening notification for a call from +1 (408) 555-0145.

If you miss your window to reply or if your solution can't tell the difference between iOS screening, a real human, or a generic machine, your app will no longer work correctly.

Prerequisites

For you to build and test this solution, you’ll need a Twilio account and a few things in place:

Building an iOS call screening flow

In these next steps, we’ll walk through planning for iOS call screening, then we’ll share a demo of the solution we’re building, then share the code. After this section we’ll show you how to run and test the solution.

Let’s start with a plan!

The four scenarios: What your code needs to handle

Your function needs to detect and adapt to four scenarios automatically:

Scenario 1: iOS Screening, No Answer, Goes to Voicemail

  • Detect the iOS call screening preamble
  • Play your identification message
  • AMD detects voicemail end
  • Leaves voicemail message

Scenario 2: iOS Screening, Person Answers

  • Detect the iOS call screening preamble
  • Play your identification message
  • Human picks up ( “Hello?”)
  • AMD detects a human
  • Passes the logic through to the conversation, with no duplicate prompts

Scenario 3: No Screening, Person Answers

  • A human answers immediately ( “Hello?”)
  • AMD detects a human
  • Your function passes through to the logic for a natural conversation
  • No screening or voicemail messages get triggered

Scenario 4: No Screening, Goes to Voicemail

  • Voicemail prompt gets detected directly
  • AMD detects a machine
  • Your function doesn’t detect the screening preamble
  • Leaves voicemail message

If you can identify the scenarios early, you don’t require complex branching code or dial logic, your function can recognize and adjust using live call data.

Here’s a demo of the logic in action:

The code: Your drop-in Twilio Function

Ready to try it? Next, we’ll walk you through the logic of the script.

Here’s the complete code of the function on GitHub. You can clone it using:

git clone git@github.com:rmc3d/iosCallScreeningTranscriptions.git

You can find the code in the iosCallScreeningTranscriptions directory.

In these next few sections, we’ll shine a spotlight on some of the parts of the code and explain what’s happening.

Key code snippets explained

Call State Management

Twilio Functions are stateless, so we manage memory in Maps to track each call’s lifecycle and events, letting us coordinate complex detection logic across multiple incoming webhooks.

// Global Maps for tracking state across webhook invocations
const callStates = new Map();           // Call progress state: INITIAL → IOS26_MONITORING → PASSTHROUGH/VOICEMAIL_DELIVERED
const callTranscripts = new Map();      // Accumulated transcript text (all speech detected so far)
const callStartTimes = new Map();       // Timestamp when call started (for elapsed time calculations)
const amdResults = new Map();           // AMD (Answering Machine Detection) results from parallel analysis (machine/human/unknown)
const processedActions = new Map();     // Set of actions we've taken (ios26_response,  ios26_response_late, human_passthrough, human_passthrough_fallback, voicemail_direct, voicemail_after_ios26, human_after_ios26)

When a new call starts, we initialize all states using initializeCallState(). As the call progresses and new webhook events arrive, we read/update these Maps.

function initializeCallState(callSid) {
  callStates.set(callSid, 'INITIAL');
  callStartTimes.set(callSid, Date.now());
  callTranscripts.set(callSid, '');
}

It’s helpful to know why this simple pattern “just works” in Twilio Functions:

  1. Multiple webhooks for the SAME call typically arrive at the SAME function instance.
  2. Function instances stay “warm” for several minutes between invocations.
  3. For production and horizontal scaling, use Redis (or similar).

Pattern-Matching Helpers

These helper functions recognize speech patterns in real-time transcriptions - distinguishing iOS26 preambles, intermediate prompts, standard voicemail greetings, and live human conversation. This is foundational for driving the adaptive call flow.

function detectIOS26Patterns(text) {
  if (!text) return false;
  const lowerText = text.toLowerCase();
  const ios26Patterns = [
    'record your name and reason for calling',
    'if you record your name',
    'name and reason for calling',
    'see if this person is available',
    // ... (other variants)
  ];
  // Return true if ANY pattern matches
  // .some() stops as soon as it finds a match
  return ios26Patterns.some(pattern => lowerText.includes(pattern));
}
function detectHumanSpeech(text) {
  if (!text) return false;
  const lowerText = text.toLowerCase();
  // EXCLUDE known non-human patterns
  // If it matches iOS 26, iOS 26 intermediate prompts or voicemail, it's NOT human
  if (detectIOS26Patterns(text) || detectVoicemailPatterns(text) || detectIntermediatePrompts(text)) {
    return false;
  }
  // ... check for human speech indicator ...
  const humanPatterns = [
    'who is this', 'what do you want', 'hold on', 'wait', 'speaking', 'this is'
  ];
  const hasHumanPattern = humanPatterns.some(pattern => lowerText.includes(pattern));
  // ... handle questions, etc. ...
  const hasQuestionMark = text.includes('?');
  const hasInteractiveQuestion = hasQuestionMark && (
    lowerText.includes('who') || 
    lowerText.includes('what') || 
    lowerText.includes('why')
  );
  return hasHumanPattern || hasInteractiveQuestion;
}

Scenario Decision Logic

This function accumulates transcribed speech, checks state, and orchestrates the detection and response for all four scenarios.

async function processTranscriptionWithScenarios(transcript, isFinal, callSid, context, screeningResponse, voicemailMessage, primaryPhrase) {
  // Accumulate transcript for context
  let accumulated = callTranscripts.get(callSid) || '';
  accumulated += ' ' + transcript.trim();
  if (accumulated.length > 300) {
    accumulated = accumulated.slice(-300);
  }
  callTranscripts.set(callSid, accumulated);
  // Scenario 1: Detect iOS 26 preamble
  if (currentState === 'INITIAL') {
    const isIOS26 = detectIOS26Patterns(transcript) || detectIOS26Patterns(accumulated);
    if (isIOS26) {
      // Check current state - if already IOS26_MONITORING, another webhook already handled this
      const currentState = getCallState(callSid);
      if (currentState === 'IOS26_MONITORING') return null;
      // Transition state immediately to claim this detection
      setCallState(callSid, 'IOS26_MONITORING');
      // Play our identification message via REST API
      await playIOS26Response(callSid, context, screeningResponse);
      return { detected: true, type: 'scenario1', action: 'ios26_response' };
    }
  }
  // Scenario 3: Early human detection, no iOS 26 detected
  if (currentState === 'INITIAL' && elapsedTime > 20 && elapsedTime < 25 && (detectHumanSpeech(transcript) || getAMDResult(callSid) === 'human')) {
    await stopTranscriptionAndPassthrough(callSid, context);
    setCallState(callSid, 'PASSTHROUGH');
    markActionProcessed(callSid, 'human_passthrough');
    return { detected: true, type: 'scenario3', action: 'passthrough' };
  }
  if (currentState === 'INITIAL' && elapsedTime > 20 && elapsedTime < 25) {
      const noIOS26InAccumulated = !detectIOS26Patterns(accumulated);
      const amdNotMachine = getAMDResult(callSid) !== 'machine_start' && getAMDResult(callSid) !== 'machine_end_beep';
      if (noIOS26InAccumulated && amdNotMachine && !hasProcessedAction(callSid, 'human_passthrough_fallback')) {
        await stopTranscriptionAndPassthrough(callSid, context);
        setCallState(callSid, 'PASSTHROUGH');
        markActionProcessed(callSid, 'human_passthrough_fallback');
        return { detected: true, type: 'scenario3_fallback', action: 'passthrough' };
      }
    }
  // ...and similar for other scenarios
}

Live call control: Adaptive call actions

Depending on what was detected, the system uses Twilio’s REST API to:

  • Play an identification message to iOS 26 screener
  • Leave a voicemail (after beep, with transcription paused)
  • Stop transcription and ‘pass through’ to the human
async function playIOS26Response(callSid, context, screeningResponse) {
  const client = context.getTwilioClient();
  const voice = context.TWILIO_VOICE || 'alice';
  const language = context.TWILIO_LANGUAGE || 'en-US';
  const twimlXml = `
    <?xml version="1.0" encoding="UTF-8"?>
    <Response>
      <Say voice="${voice}" language="${language}">${screeningResponse}</Say>
      <Start>
        <Transcription 
          track="inbound_track" 
          transcriptionEngine="google" 
          speechModel="telephony"
          partialResults="true"
          statusCallbackUrl="https://${context.DOMAIN_NAME}/ios26-callScreeningDetection"
          name="post-ios26-monitoring"
        />
      </Start>
      <Pause length="300"/>
    </Response>`;
  // Sending iOS 26 screening response to the call
  await client.calls(callSid).update({ twiml: twimlXml });
}
async function leaveVoicemailMessage(callSid, context, voicemailMessage) {
  const client = context.getTwilioClient();
  const voicemailTwiml = `
    <?xml version="1.0" encoding="UTF-8"?>
    <Response>
      <Stop>
        <Transcription name="ios26-full-detection"/>
        <Transcription name="post-ios26-monitoring"/>
      </Stop> 
      <Pause length="10"/>
      <Say>${voicemailMessage}</Say>
      <Pause length="2"/>
    </Response>
  `;
  await client.calls(callSid).update({ twiml: voicemailTwiml });
}
async function stopTranscriptionAndPassthrough(callSid, context) {
  const client = context.getTwilioClient();
  const stopTwiml = `
    <?xml version="1.0" encoding="UTF-8"?>
    <Response>
      <Stop>
        <Transcription name="ios26-full-detection"/>
        <Transcription name="post-ios26-monitoring"/>
      </Stop>
      <Pause length="300"/>
    </Response>
  `;
  await client.calls(callSid).update({ twiml: stopTwiml });
}

How to deploy and invoke it

  1. Create a new Function in one of your Services in Twilio Functions and Assets with the name ios26-callScreeningDetection
  2. Copy the functionto the created file
  3. (Optional) Set these environment variables if you want to personalize the messages:
  4. SCREENING_RESPONSE: The text played on screening ( "This is Twilio calling...")
  5. VOICEMAIL_MESSAGE: The voicemail body
  6. (Optional) Set the IOS26_PRIMARY_PHRASE environment variable if you want to change the canonical phrase to trigger iOS 26
  7. Save and Deploy your function
  8. Invoke the function
  9. Watch your logs for end-to-end detection, branching, and voicemails.

Get the Function’s domain name, you’ll need it to invoke it when making a call.

Screenshot of Twilio console showing call screening test environment settings and options.

Test the Function with the Twilio CLI

And here’s how to invoke with the Twilio CLI:

twilio api:core:calls:create \
  --from "+YOUR_NUMBER" \
  --to "+CUSTOMER_NUMBER" \
  --url "https://YOUR_DOMAIN.twil.io/ios26-callScreeningDetection" \
  --machine-detection "Enable" \
  --async-amd true \
  --async-amd-status-callback "https://YOUR_FUNCTION_DOMAIN.twil.io/ios26-callScreeningDetection" \
  --async-amd-status-callback-method "POST" \
  --machine-detection-timeout 30 \
  --status-callback "https://YOUR_FUNCTION_DOMAIN.twil.io/ios26-callScreeningDetection \
  --status-callback-event "initiated" "ringing" "answered" "in-progress" "completed" \
  --status-callback-method "POST"

With that CLI call, the function automatically:

  • Kicks off transcription on the callee's inbound audio
  • Continuously accumulates transcript text to run phrase detection
  • Monitors Async AMD in parallel
  • Knows exactly when to inject your ID message, when to stop transcription, when to leave voicemail, and when to connect to a real human

Example: Logs that help you debug

Make sure the Live Logs are enabled on your function and you’ll see exactly what happened in the Twilio Function logs. For example:

🔔 WEBHOOK RECEIVED at 2024-06-19T10:04:17.000Z
📞 Call SID: CAxxxxxxxxxxxxxxxxxxxx
💬 TRANSCRIPT: "hi if you record your name and reason for calling..."
🎯 iOS 26 PREAMBLE DETECTED → Action: play identification
📬 Voicemail detected after iOS 26 response → Action: leave message
✅ Voicemail message sent
🧹 Cleaned up state for call CAxxxxxxxxxxxxxxxxxxxx

Conclusion

You’ve just learned how to reliably detect and respond to iOS 26 call screening using Twilio AMD and Real-Time Transcription, no need for guesswork or manual handling. With this function, you can now ensure your outbound voice calls reach real people more efficiently and handle all scenarios automatically.

Further Resources


Rosina Garcia Bru is a Product Manager at Twilio Programmable Voice. Passionate about creating seamless communication experiences. She can be reached at rosgarcia [at] twilio.com

Robert McCulley is a Product Manager at Twilio focused on Trusted Voice communications. Passionate about protecting the simple human connection behind every phone call by working to restore trust in the PSTN and driving telecom fraud to zero. He can be reached at rmcculley [at] twilio.com