What callers get
-
A billing menu
Press 1 to pay an invoice, 2 for the account balance, 3 to dispute a missing TPS report, or 4 for management.
-
Invoice entry
The caller keys in an invoice number and hears it read back, digit by digit, with the balance due.
-
A choice of amount
Pay the full $495.00 balance, or only the $45.00 TPS report processing surcharge.
-
Card capture with <Pay>
Card number, expiration date, security code, and ZIP code, keyed on the phone’s keypad and charged through your Stripe account. Your documents never see the card digits.
-
A truthful closing
The caller hears a closing message that holds whether the charge was approved or declined, followed by an update on the receipt printer.
How the pieces fit
| Piece | Where it lives | What it does |
|---|---|---|
| Main document | voiceml.org, /r/ URL | The number’s voice URL. Plays the menu and branches on the key pressed. |
| Invoice document | voiceml.org, /r/ URL | Reads back the invoice number and the balance, and offers the two amounts. |
| Payment document | voiceml.org, /r/ URL | Sets the amount from the key pressed and runs <Pay>. |
| Confirmation document | voiceml.org, /s/ URL | The action of <Pay>. Closes the call. |
| Stripe account | Its test-mode secret key, saved on the Payments page in the VoiceML Portal | Receives the card details and makes the charge. |
Before you start
- A VoiceML account with a phone number. See VoiceML or start a free trial.
- Payments enabled on the account. Payments are off by default. Ask us to turn them on; the VoiceML Portal then shows a Payments page.
- A Stripe test-mode secret key. In Stripe, open Developers, then API keys, and reveal the secret key that starts with
sk_test_. Paste it on the Payments page. The key is write-only, and each account’s payments go to its own Stripe account; there is no shared key.
Use Stripe test cards only. Test-mode charges don’t move money, and Stripe’s guidance is never to test with real card details. Never enter a real card number when you call this line.
Step 1: Publish the confirmation document
Published documents can’t be edited, and each one needs the URL of the document after it, so publish from the end of the call backwards. The confirmation document is the end: <Pay> requests it when the payment finishes.
confirm.xml
<Response>
<Say>Thank you. That completes the Initech payment process. If your payment was approved, it will appear on your next statement.</Say>
<Pause length="1"/>
<Say>We tried to print your receipt, but the printer says PC load letter, and nobody here knows what that means.</Say>
<Pause length="1"/>
<Say>Thank you for calling Initech. Goodbye.</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 payment document
Replace CONFIRM_URL with the URL from step 1, then publish.
pay.xml
<Response>
{% if Digits == "1" %}
{% assign amount = "495.00" %}
{% assign spoken = "four hundred ninety-five dollars" %}
{% elsif Digits == "2" %}
{% assign amount = "45.00" %}
{% assign spoken = "forty-five dollars" %}
{% endif %}
{% if amount == blank %}
<Say>That is not a valid option. Goodbye.</Say>
<Hangup/>
{% else %}
<Say>You are paying {{ spoken }}.</Say>
<Pay chargeAmount="{{ amount }}" currency="usd" description="Initech invoice payment"
action="CONFIRM_URL" method="POST" maxAttempts="2" timeout="10">
<Prompt for="payment-card-number">
<Say>Please enter your card number, followed by the pound sign.</Say>
</Prompt>
<Prompt for="expiration-date">
<Say>Please enter the four-digit expiration date, two digits for the month and two for the year.</Say>
</Prompt>
<Prompt for="security-code">
<Say>Please enter the security code on your card, followed by the pound sign.</Say>
</Prompt>
<Prompt for="postal-code">
<Say>Please enter your five-digit billing ZIP code.</Say>
</Prompt>
<Prompt for="payment-processing">
<Say>Please hold while we process your payment. Yeah, if you could go ahead and stay on the line, that would be great.</Say>
</Prompt>
</Pay>
{% 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 invoice document
Replace PAY_URL with the /r/ URL from step 2, then publish.
invoice.xml
<Response>
{% if Digits == blank %}
<Say>We did not receive an invoice number. Goodbye.</Say>
<Hangup/>
{% else %}
<Say>Invoice number {{ Digits | split: "" | join: " " }}.</Say>
<Pause length="1"/>
<Say>The balance due is four hundred ninety-five dollars. That includes a forty-five dollar TPS report processing surcharge.</Say>
<Gather input="dtmf" numDigits="1" action="PAY_URL" method="POST" timeout="5">
<Say>To pay the full balance of four hundred ninety-five dollars, press 1.</Say>
<Say>To pay only the TPS report surcharge of forty-five dollars, press 2.</Say>
</Gather>
<Say>We did not receive a selection. Goodbye.</Say>
<Hangup/>
{% endif %}
</Response>
Step 4: Publish the main document and point the number at it
Replace INVOICE_URL with the /r/ URL from step 3, publish, and set the resulting /r/ URL as the phone number’s voice URL. GET and POST both work.
main.xml
<Response>
{% if Digits == blank %}
<!-- Main menu -->
<Gather input="dtmf" numDigits="1" timeout="5" method="POST">
<Pause length="1"/>
<Say>Thank you for calling Initech, where we are committed to maximizing productivity and minimizing unnecessary human interaction.</Say>
<Pause length="1"/>
<Say>To pay an outstanding invoice, press 1.</Say>
<Say>To check your account balance, press 2.</Say>
<Say>To dispute a charge for a missing TPS report, press 3.</Say>
<Say>To speak with management, press 4.</Say>
<Pause length="1"/>
<Say>And remember, we are going to need you to go ahead and make that payment. That would be great.</Say>
</Gather>
<Say>We did not receive a selection. Goodbye.</Say>
<Hangup/>
<!-- Press 1: pay an invoice -->
{% elsif Digits == "1" %}
<Gather input="dtmf" action="INVOICE_URL" method="POST" finishOnKey="#" timeout="5">
<Say>Please enter your invoice number, followed by the pound sign.</Say>
</Gather>
<Say>We did not receive an invoice number. Goodbye.</Say>
<Hangup/>
<!-- Press 2: account balance -->
{% elsif Digits == "2" %}
<Say>Your account balance is four hundred ninety-five dollars. That includes a forty-five dollar TPS report processing surcharge, because your last report was filed without the new cover sheet.</Say>
<Gather input="dtmf" numDigits="1" timeout="5" method="POST">
<Say>To pay it now, press 1.</Say>
</Gather>
<Say>Thank you for calling Initech. Goodbye.</Say>
<Hangup/>
<!-- Press 3: TPS report dispute -->
{% elsif Digits == "3" %}
<Say>Disputes for a missing TPS report must be filed with the new cover sheet. Did you get the memo? We will go ahead and make sure you get another copy of that memo.</Say>
<Gather input="dtmf" numDigits="1" timeout="5" method="POST">
<Say>To pay your invoice instead, press 1.</Say>
</Gather>
<Say>Thank you for calling Initech. Goodbye.</Say>
<Hangup/>
<!-- Press 4: management -->
{% elsif Digits == "4" %}
<Say>Your call is important to all eight of your bosses. Each of them will follow up with you separately to discuss it.</Say>
<Gather input="dtmf" numDigits="1" timeout="5" method="POST">
<Say>While you wait, to pay your invoice, press 1.</Say>
</Gather>
<Say>Thank you for calling Initech. Goodbye.</Say>
<Hangup/>
<!-- Press 0: not on the menu -->
{% elsif Digits == "0" %}
<Say>Our operator has been moved to the basement. If you have seen a red stapler, please return it there.</Say>
<Gather input="dtmf" numDigits="1" timeout="5" method="POST">
<Say>To pay your invoice, press 1.</Say>
</Gather>
<Say>Thank you for calling Initech. Goodbye.</Say>
<Hangup/>
{% else %}
<Gather input="dtmf" numDigits="1" timeout="5" method="POST">
<Say>That is not a valid option. Please press 1, 2, 3, or 4.</Say>
</Gather>
<Say>We did not receive a selection. Goodbye.</Say>
<Hangup/>
{% endif %}
</Response>
Step 5: Test it
- Call the number and press 1.
- Enter any invoice number, such as 1999, then press #.
- Press 1 to pay the full balance.
- Enter Stripe’s test Visa number,
4242 4242 4242 4242, then #. For the expiration date, enter any future month and year as four digits, such as1234; then any three-digit security code and #; then any five-digit ZIP code. - In the Stripe Dashboard, look at your test payments: a $495.00 payment with the description “Initech invoice payment”.
Call again with Stripe’s generic decline card, 4000 0000 0000 0002, to hear that the closing message still holds when the charge is declined. Stripe lists more test cards on its testing page.
How the call flows
The first request for the main document carries no Digits, so {% if Digits == blank %} plays the menu inside a <Gather numDigits="1">. That <Gather> has no action, so the key the caller presses comes back to the same document as Digits and picks a branch. The branches for 2, 3, 4, and 0 end with the same kind of <Gather>, so pressing 1 there also leads to the payment.
| Caller presses | What happens |
|---|---|
| 1 | Asks for the invoice number. This <Gather> has action set to the invoice document and ends on #. |
| 2 | Reads the $495.00 balance and the TPS report surcharge, then offers 1 to pay. |
| 3 | Explains the TPS report cover-sheet policy, then offers 1 to pay. |
| 4 | Promises a follow-up from each of the caller’s eight bosses, then offers 1 to pay. |
| 0 | Not on the menu. Reports where the operator has gone, asks about a red stapler, then offers 1 to pay. |
| Anything else | “That is not a valid option,” then asks again for 1, 2, 3, or 4. |
The invoice document reads the number back with {{ Digits | split: "" | join: " " }}, so 1999 is spoken as “1 9 9 9”, not “one thousand nine hundred ninety-nine”. Its <Gather> sends the amount choice to the payment document, which turns 1 or 2 into an amount with {% assign %} and hands it to <Pay>:
| Attribute | Value | Effect |
|---|---|---|
chargeAmount | 495.00 or 45.00 | Charges the card. Without it, <Pay> only tokenizes the card. |
currency | usd | The charge currency (the default, set here for clarity). |
description | Initech invoice payment | Shown with the payment in Stripe. |
action | The confirmation document | Requested when the payment finishes, approved or not. |
maxAttempts | 2 | Gives the caller two tries at each entry. The limit is 1 to 3. |
timeout | 10 | Seconds to wait for the caller’s next digit, up from the default of 5, so callers have time to read the card as they key it in. |
Each <Prompt for="…"> replaces the built-in prompt for one step: payment-card-number, expiration-date, security-code, postal-code, and payment-processing, which plays while Stripe processes the charge. Visa, Mastercard, and American Express are accepted by default; list others in validCardTypes.
Why the closing message doesn’t say “approved”
When the payment finishes, <Pay> requests its action with the outcome: Result, PaymentConfirmationCode, and the card’s last four digits in PaymentCardNumber. A voiceml.org document can read the call’s parameters and a <Gather>’s results, but not these, and an /s/ document never reads the request at all. So the confirmation document can’t tell an approved payment from a declined one, and it says only what is true either way: “If your payment was approved, it will appear on your next statement.”
To read back the confirmation code, or to send a declined caller back to try another card, point action at your own server. It receives the same parameters and can answer with VoiceML of its own.
Where the movie turns up
| Where | Reference |
|---|---|
| The greeting | “We are going to need you to go ahead and make that payment. That would be great.” |
| Options 2 and 3 | The TPS report and its new cover sheet. Did you get the memo? |
| Option 4 | All eight of your bosses. |
| Option 0, not on the menu | An operator moved to the basement, and a red stapler. |
| While the charge processes | “Yeah, if you could go ahead and stay on the line, that would be great.” |
| The closing message | The receipt printer, which says PC load letter. |
Limits of this design
- Every invoice has the same balance. A voiceml.org document can branch on the call but can’t look anything up, so any invoice number gets $495.00. Looking up a real balance needs a server.
- The invoice number doesn’t reach Stripe. Values don’t carry from one document to the next, so the payment document never sees the invoice number and its
descriptionis fixed text. - The closing message can’t report the result. See above; reporting it needs a server as the
action. - Keypad entry only.
<Pay>takes card details as keypad digits; spoken card entry isn’t available. - Written for test mode. Before taking real card payments, talk to us.
Credits
- Initech, its TPS reports, the cover-sheet memo, the eight bosses, the red stapler, and the printer come from Office Space (1999), written and directed by Mike Judge and distributed by 20th Century Fox. Ron Livingston stars as Peter Gibbons, the Initech programmer with eight different bosses; Gary Cole plays vice president Bill Lumbergh, whose “that would be great” the greeting borrows, and Stephen Root plays Milton Waddams, the collator with the red stapler.
- Initech, its billing department, its invoices, and its balances are fictional in this example. VoiceTel is not affiliated with the film or its rights holders.
- References: the
<Pay>reference, the VoiceML language reference, the voiceml.org publishing and integration guide, and Stripe’s testing documentation.

