Build an order-tracking line
In this tutorial, you build a phone line for a delivery company that lets callers check on an order by entering a tracking number. The line greets the caller, collects a ten-digit tracking number with <Gather>, reads back the order's status and estimated delivery with <Say>, lets the caller repeat the details or ask for an agent, and connects to a live agent with <Dial> when requested. By the end, you have a complete server, in Node.js or Python, that handles every step of the call across four routes:
/voice: greets the caller and collects a tracking number./track: looks up the tracking number and reports its status, or apologizes and starts over when it does not recognize the number./next: repeats the status or connects the caller to an agent, based on what the caller presses next./agent-result: reports back once the agent call ends, in case no agent answers.
Time to complete: 30 minutes
Prerequisites
- Node.js 18 or later, or Python 3.9 or later, depending on which language you follow.
- A phone number on your account with a configurable voice URL. See Receiving calls if you do not yet have one.
- A tunneling tool that exposes a local port as a public HTTPS URL, used in the last step to let PressOne reach your local server.
Familiarity with Gather, Dial, and Redirect helps but is not required. This tutorial links to the relevant reference section at each step.
Step 1: Scaffold the server
Start with the application shell and the order data the rest of the tutorial reads from. orders stands in for a database lookup, keyed by a ten-digit tracking number, with a status and an eta field for each order. attempts is a second in-memory store, keyed by CallSid, that later steps use to count how many times a caller has entered a tracking number that does not match anything in orders.
Save the following as server.js (Node.js) or app.py (Python):
This server runs but has no routes yet. Each of the following steps adds one route and shows the file again with that route in place.
Step 2: Greet the caller and collect a tracking number
/voice is the route you eventually put on the number's voice URL. It greets the caller and collects exactly ten digits with <Gather>, then sends them to /track:
Code
With numDigits="10", gathering ends the instant the caller enters the tenth digit: the caller does not need to press # afterward. If the caller enters nothing before the timeout elapses, gathering ends with no result. This <Gather> leaves actionOnEmptyResult at its default of false, so PressOne does not request /track in that case at all: execution falls through directly to the next verb in this same document, the fallback <Say> and <Redirect> back to /voice. Setting actionOnEmptyResult="true" would instead request /track with Digits absent from the request, which is useful when the lookup handler itself needs to react to silence, but is unnecessary here since the retry lives in this same document.
Add the route to the server from Step 1:
Step 3: Look up the tracking number
/track reads the collected digits, looks the tracking number up in orders, and branches on whether it finds a match. On a match, it reads back the status and ETA, then opens a second, single-digit <Gather> that offers to repeat the details or connect to an agent:
Code
The action URL carries the tracking number as a query parameter (/next?tn=4825601937) because /next (built in the next step) needs to know which order the caller is asking about, and the digit it receives is the menu selection, not the tracking number. Action URLs can carry a query string like this one; PressOne requests the URL as given, query string included, and your route reads tn from the query string alongside whatever PressOne posts in the request body.
When the tracking number does not match an order, /track apologizes and redirects back to /voice so the caller can try again:
Code
OneML caps a document at 10 consecutive redirects that happen without new caller input, ending the call with an error past that limit. See Redirect. Each retry here only happens after the caller enters another set of digits, and a redirect triggered by renewed caller input resets the counter, so repeated wrong guesses do not, on their own, run into that limit. Still, prompting the same caller for a tracking number indefinitely is a poor experience well before ten attempts. /track tracks how many times each call, keyed by CallSid, has failed to find a match in the attempts map from Step 1, and after three misses ends the call instead of asking a fourth time:
Code
Add the route:
Step 4: Repeat the details or connect an agent
/next reads the single digit collected by the <Gather> in /track, along with the tn query parameter carried on its own action URL. Pressing 1 replays the order by redirecting back to /track, passing the tracking number through the same query-parameter pattern:
Code
/track already reads tn from the query string ahead of Digits, so this redirect reruns the same lookup and Says the same status and ETA again, without asking the caller to re-enter the tracking number.
Pressing 2 dials a support number and sets action to /agent-result so the server hears back once the dial ends:
Code
/agent-result reads DialCallStatus from that request. A value of completed means the call bridged and has already ended normally, so the handler hangs up without an announcement. Any other value (busy, no-answer, canceled, or failed) means no agent picked up, so the handler apologizes instead:
Code
Add both routes. This is the complete, assembled server:
Every path the document reaches leads to a route defined above: /voice gathers into /track; /track gathers into /next or redirects back to /voice; /next redirects back to /track or dials into /agent-result; /agent-result always ends the call.
Replace +2341700000000 and +2341800000001 with a number your account owns and a real number you can answer, respectively, before testing the agent leg end to end. +2341700000000 must belong to your account: PressOne fails the dial with DialCallStatus of failed otherwise, which routes to the same apology as busy or no-answer.
Step 5: Test the line locally
Start the server:
It listens on port 3000 for the four routes built above. Expose that port to the internet with a tunneling tool of your choice. Most such tools print a public HTTPS URL that forwards to your local server. Copy that URL for the next step.
In your dashboard, open the configuration for the number you are testing with, and set its voice URL to the public URL from your tunnel, followed by /voice, with POST as the method (for example https://YOUR-TUNNEL-URL/voice).
Call the number from any phone.
Verify the result
You should hear: "Welcome to Acme Delivery. Enter your ten digit tracking number." Enter 4825601937, one of the tracking numbers seeded in Step 1. PressOne reads back "Your order is out for delivery. Estimated delivery: today by 6:00 PM," followed by "To hear this again, press 1. To speak with an agent, press 2."
- Press
1: PressOne repeats the same status and ETA. - Press
2: PressOne dials the number configured in/next, and either bridges the call or plays the fallback apology from/agent-result, depending on whether it answers. - Enter a tracking number that is not in
ordersthree times in a row: the first two attempts apologize and return you to the greeting; the third ends the call with a final apology instead of asking again. - Enter nothing at the initial prompt and let it time out: PressOne says it did not receive a tracking number and returns you to the greeting.
Troubleshooting
PressOne never requests your server
Confirm the tunnel is still running and printing the same public URL configured on the number's voice URL, and that the voice URL's method is set to POST to match the routes above, which only handle POST. Check your tunneling tool's own request log. If it shows no incoming requests, the number's voice URL configuration is the more likely cause; if it shows requests but your server never logs them, check that the server is running on port 3000.
The call ends abruptly with an error partway through
This usually means a route returned something that is not well-formed XML, or omitted the text/xml content type. Double-check that every branch calls res.type("text/xml").send(oneml) or returns Response(oneml, mimetype="text/xml"), and that any value interpolated into an oneml string (an order status, an ETA) does not itself contain characters like < or & that would break the XML.
Pressing 2 does not connect a call
/next dials the placeholder numbers +2341700000000 and +2341800000001. Replace both with a number your account owns and a real number you can answer, as noted in Step 4, before testing this branch.
The caller reaches the final apology sooner than expected
attempts is an in-memory map that resets whenever the server restarts. If you restart the server between calls while testing the not-found path, a caller who previously had one or two failed attempts recorded starts over at zero on the next call: this is expected during local testing, but plan for a persistent store, such as your call database, if you carry this pattern into production.
Next steps
- Gather: the complete attribute and action-parameter reference.
- Dial: the complete attribute and action-parameter reference.
- Redirect: the redirect-loop limit and method options.
- Forwarding calls: more on choosing a caller ID and ringing multiple numbers.
- Webhooks: verify that requests to routes like these genuinely came from PressOne before trusting them in production.