VoiceTel

Support / VoiceML examples / Build call screening with VoiceML

Build call screening with VoiceML.

The McCallisters are a fictional family with a house phone to protect. Known nuisance numbers are refused before the call is answered, family numbers ring straight through, and every other caller says their name and why they’re calling, which speech recognition checks before anyone is disturbed. It runs on three documents published on voiceml.org, with no application server.

Free trial <Gather> reference

What callers get

  • A block list

    Numbers on the block list hear a busy signal. <Reject> refuses the call before it is answered, so it isn’t charged.

  • A family list

    Calls from family numbers ring the house without any questions.

  • Spoken screening

    Everyone else says their name and the reason for the call. Speech recognition turns it into text, and the next document decides what to do with it.

  • Rules on what was said

    Family calling from an unknown number and the pizza delivery are put through. The Wet Bandits are refused by name, whatever number they use.

  • Voicemail for the rest

    Callers who match no rule leave a message instead of ringing the house.

How the pieces fit

Components of the McCallister call screening
Piece Where it lives What it does
Main documentvoiceml.org, /r/ URLThe number’s voice URL. Checks the caller’s number against both lists, and asks everyone else who they are.
Screening documentvoiceml.org, /r/ URLThe action of the spoken <Gather>. Matches what the caller said and connects, refuses, or takes a message.
Goodbye documentvoiceml.org, /s/ URLThe action of the <Record>. Says goodbye and hangs up.
House phoneAny phone numberThe phone that rings for callers who are put through.

Before you start

  1. A VoiceML account with a phone number. See VoiceML or start a free trial.
  2. Speech recognition. Nothing to set up for English: accounts without their own speech recognizer use the platform default, which recognizes English only. To use another engine or language, see speech recognition.
  3. The phone that should ring, and the numbers for the block and family lists, in E.164 format.

The numbers in these documents are fictional. They use the 555-0100 to 555-0199 range, which is set aside for fiction. Replace them with real numbers before you publish.

Step 1: Publish the goodbye document

Every <Record> needs an action; without one, the runtime requests the current document again and the caller records a second time. This document is that action.

goodbye.xml

<Response>
  <Say>Thank you. The McCallisters will call you back. Merry Christmas.</Say>
  <Hangup/>
</Response>

Paste it at voiceml.org, validate it, and publish. It contains no Liquid, so it is served from an /s/ URL. Keep the URL exactly as returned, key included. The key is a shared secret, so don’t post the URL publicly.

Step 2: Publish the screening document

Replace GOODBYE_URL with the URL from step 1 and the house number with your own, then publish.

screen.xml

<Response>
  {% assign said = SpeechResult | downcase %}
  {% assign confidence = Confidence | plus: 0 %}

  {% if said == blank %}
    <Say>We did not hear anything. Goodbye.</Say>
    <Hangup/>

  <!-- The Wet Bandits are refused, whatever number they call from -->
  {% elsif said contains "harry" or said contains "marv" or said contains "wet bandit" %}
    <Say>The Wet Bandits are not welcome at this residence. The house is protected. Goodbye.</Say>
    <Hangup/>

  <!-- Unsure what was said: ask again. With no action, the answer comes back to this document.
       A confidence of 0 means the speech engine didn't report one, so it is not treated as unsure. -->
  {% elsif confidence &gt; 0 and confidence &lt; 0.5 %}
    <Gather input="speech" method="POST" timeout="5" speechTimeout="auto"
            hints="Kate, Peter, Buzz, Uncle Frank, Little Nero's, pizza">
      <Say>Sorry, we did not catch that. Please say your name and the reason for your call.</Say>
    </Gather>
    <Say>We did not hear anything. Goodbye.</Say>
    <Hangup/>

  <!-- Family calling from a number we don't know, and the pizza delivery, are put through -->
  {% elsif said contains "kate" or said contains "mom" or said contains "peter" or said contains "dad" or said contains "buzz" or said contains "frank" %}
    <Say>Thank you. Connecting you now.</Say>
    <Dial timeout="20" ringTone="us">
      <Number>+18475550100</Number>
    </Dial>
    <Say>If no one picked up, please try again later. Goodbye.</Say>
    <Hangup/>

  {% elsif said contains "nero" or said contains "pizza" %}
    <Say>Little Nero's. Connecting you now. And keep the change, ya filthy animal.</Say>
    <Dial timeout="20" ringTone="us">
      <Number>+18475550100</Number>
    </Dial>
    <Say>If no one picked up, please leave the pizza on the front step. Goodbye.</Say>
    <Hangup/>

  <!-- Everyone else goes to voicemail -->
  {% else %}
    <Say>Thank you. No one can come to the phone right now. Please leave a message after the tone.</Say>
    <Record action="GOODBYE_URL" method="POST" maxLength="120" timeout="5" playBeep="true" trim="trim-silence"/>
  {% endif %}
</Response>

It contains Liquid tags, so it is published to an /r/ URL and rendered on every call with that call’s parameters.

Step 3: Publish the main document and point the number at it

Replace SCREEN_URL with the /r/ URL from step 2, and fill in your block list, family list, and house number. Publish it, and set the /r/ URL it returns as the phone number’s voice URL.

main.xml

<Response>
  {% assign blocked = "+18475550188,+18475550189" | split: "," %}
  {% assign family = "+18475550101,+18475550102,+18475550103" | split: "," %}

  {% if blocked contains From %}
    <!-- Known nuisance numbers: refused before the call is answered -->
    <Reject reason="busy"/>

  {% elsif family contains From %}
    <!-- Family numbers ring the house without screening -->
    <Dial timeout="20" ringTone="us">
      <Number>+18475550100</Number>
    </Dial>
    <Say>If no one picked up, please try again later. Goodbye.</Say>
    <Hangup/>

  {% else %}
    <!-- Everyone else is screened -->
    <Gather input="speech" action="SCREEN_URL" method="POST" timeout="5" speechTimeout="auto"
            hints="Kate, Peter, Buzz, Uncle Frank, Little Nero's, pizza, Harry, Marv">
      <Say>You have reached the McCallister residence. Please say your name and the reason for your call.</Say>
    </Gather>
    <Say>We did not hear anything. Goodbye.</Say>
    <Hangup/>
  {% endif %}
</Response>

Step 4: Test it

  1. Call from a number on neither list and say “This is Kate, calling about Kevin.” The house phone rings.
  2. Call again and say “I’m calling about your car’s extended warranty.” You are asked to leave a message.
  3. Add your own number to the family list, publish again, repoint the number, and call: the house phone rings with no questions.

How the screening works

The main document compares From, the caller’s number in E.164, with two lists built by {% assign blocked = "+18475550188,+18475550189" | split: "," %}. Liquid’s contains tests whether a list holds a value, so {% if blocked contains From %} is the whole check. A blocked caller gets <Reject reason="busy"/>. It comes before any other verb, so the call is refused without being answered.

Everyone else hits a <Gather input="speech"> whose action is the screening document. That document receives what the caller said as SpeechResult, lower-cases it, and tests it with contains, in this order:

How the screening document decides
If the caller said Then
Nothing“We did not hear anything,” and the call ends.
“Harry”, “Marv”, or “Wet Bandit”Refused, and the call ends.
Anything, with a Confidence above 0 but under 0.5Asked to say it again.
“Kate”, “Mom”, “Peter”, “Dad”, “Buzz”, or “Frank”Put through to the house.
“Nero” or “pizza”Put through, with a word for the delivery driver.
Anything elseAsked to leave a message.

The hints attribute lists the names and words the rules look for, which biases recognition toward them. The “say it again” prompt is a <Gather> with no action, so the second answer comes back to the screening document and goes through the same rules. Some speech engines don’t report a confidence and send 0, so 0 isn’t treated as unsure; otherwise every caller on those engines would be asked again and again.

Where the movie turns up

References to Home Alone in the call flow
Where Reference
The family listKate, Peter, Buzz, and Uncle Frank, the McCallisters who left for Paris.
The refusalHarry and Marv, the Wet Bandits.
The pizza deliveryLittle Nero’s, and “Keep the change, ya filthy animal” from Angels with Filthy Souls, the gangster film Kevin plays at the door.
The goodbye“Merry Christmas.”

Limits of this design

  • The person answering doesn’t hear who is calling. The rules decide who gets through; the name the caller spoke isn’t played to the house phone.
  • No caller name lookup. The VoiceML runtime doesn’t send CallerName, so screening uses the caller’s number and what they say.
  • Caller ID can be faked. The block and family lists keep out casual nuisance calls, but they aren’t security: anyone who sets their caller ID to a family number rings straight through.
  • Matching is by keyword. contains "frank" also matches “Franklin” and “frankly”. Choose words your real callers will say and strangers won’t.
  • The “say it again” prompt can repeat. A caller the recognizer stays unsure about is asked again each time; documents can’t count attempts.
  • Messages stay on your account. To have them emailed, add the VoiceML Dashboard’s voicemail callback to the <Record>, as the PBX example does.

Credits

  • The McCallisters, the Wet Bandits, and Little Nero’s come from Home Alone (1990), directed by Chris Columbus, written and produced by John Hughes, and distributed by 20th Century Fox. Macaulay Culkin plays Kevin McCallister; Joe Pesci and Daniel Stern play Harry and Marv; Catherine O’Hara and John Heard play Kate and Peter.
  • The household, its lists, and its phone numbers are fictional in this example. VoiceTel is not affiliated with the film or its rights holders.
  • References: the <Gather> reference, the <Reject> reference, and the voiceml.org publishing and integration guide.

Trusted by