Keypad and IVR
"Press 1 to confirm." A keypad menu answers in milliseconds because nothing in the turn is a network call — no recognizer, no model, no voice synthesis. It is also the part of a call that keeps working on the day a speech vendor is having an outage.
Two ways to use it
An agent in keypad mode | Keypad on a
conversation agent | |
|---|---|---|
| What it is | A menu and nothing else | Shortcuts alongside a real conversation |
| Speech | Never listened to | Listened to as normal |
| Costs per turn | Nothing | Nothing, for the keyed turns |
| Reach for it when | The whole job is a fixed menu | You want "press 9 for a person" to work at any point |
The keypad map is read in both cases. The mode decides whether anything
else is.
Building a menu
/v1/agents/{id}curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "keypad",
"languages": [{
"code": "en-IN",
"greeting": "Thanks for calling Acme. Press 1 to confirm your appointment, 2 to cancel it, or 9 to speak to somebody."
}],
"keypad": {
"1": { "say": "Your appointment is confirmed. We will see you then. Goodbye.", "end_call": true },
"2": { "say": "Your appointment has been cancelled. Goodbye.", "end_call": true },
"9": { "say": "Putting you through now." }
},
"transfer_number": "+912261234567",
"no_match": "Sorry, that is not one of the options.",
"no_input": "Press 1 to confirm, 2 to cancel, or 9 to speak to somebody.",
"max_no_input": 2,
"keypad_timeout_seconds": 6
}'The entries
| Field | Type | Description |
|---|---|---|
keypadoptional | object | Digit to action:
{"1": {"say": "…", "end_call": true}}. Keys are single characters —
0–9, * and #. Replaces the whole
map when sent, so read-modify-write to add one entry. |
sayoptional | string | Spoken back when that key is pressed. Supports
{{variables}}. |
end_calloptional | boolean | Hangs up once say has been spoken in
full — not the instant the line ends, so the caller hears all of it. |
transferoptional | boolean | Puts the caller through to a person once
say has played — "press 1 for sales". Goes to transfer_to, or to
the agent's transfer_number when that is empty. Needs a carrier that can
redirect a live call; where the call cannot be put through it ends as
transfer_unavailable or transfer_failed rather than leaving the
caller in the menu. |
transfer_tooptional | string | This key's own destination, E.164. Lets 1 and 2 reach different teams. |
no_matchoptional | string | Spoken when the caller presses something with no entry. Three in a row ends the call. |
no_inputoptional | string | Spoken when nothing is pressed — usually the menu again. |
max_no_inputoptional | integer | How many times to re-read the menu to a
silent caller before giving up. The call ends
no_input. |
keypad_timeout_secondsoptional | integer | How long to wait for a press before treating it as silence. |
Handing a menu over to a conversation
An entry with no end_call simply speaks and waits. On a
conversation agent that is where the model takes over — which is how "press 3 for
billing" leads into an actual billing conversation rather than a dead end.
{
"mode": "conversation",
"keypad": {
"9": { "say": "Putting you through now.", "transfer": true },
"3": { "say": "Sure — tell me what the billing question is." }
}
}Pressing 9 hands the call to a person — at the agent's
transfer_number, or at the entry's own transfer_to. See
transfers and endings.
Digits arrive two different ways
This is the part that makes keypad handling look flaky when it is not, and it is worth understanding before debugging anything.
| Way | What happens |
|---|---|
| Signalled by the carrier | The carrier tells us a key was pressed, out of band. Reliable, and the usual case on a real telephone. |
| Heard in the audio | The tone is in the media stream and is detected there. This is what a browser call produces — a web dialpad generates real tones into the audio, because there is no carrier to signal on its behalf. |
Both end up in the same place. Every press writes a call.dtmf line to the call's
events saying which key it was and what became of it — matched an entry, matched nothing, arrived
too late. When a menu "sometimes works", that line is the answer: it will show which of the two
paths that call took.
/v1/calls/{id}/eventscurl -sN https://voice.sphoro.com/v1/calls/$CALL_ID/events \
-H "Authorization: Bearer $SPHORO_API_KEY" | grep dtmfOn Vobiz
Vobiz does neither: its media stream carries no key events, and mobile networks send the
key out of band, so no tone reaches the audio. A keypad agent on Vobiz therefore
does not stream. Vobiz runs the menu itself — it plays each line in the agent's voice, collects
the key, and asks this platform what the key does. Keys, lines, transfers and endings behave as
they do anywhere else and are written to the transcript the same way. Two things differ: the
call is not recorded, and a key pressed while a line is playing is taken when the line
finishes. A conversation agent's keypad entries cannot be pressed on Vobiz; use
Plivo or Twilio for those, or let callers say the option.
Keying ahead
A keypress always interrupts, whatever interrupt_words says, and it interrupts
the greeting too. That matters more than it sounds: every regular caller keys
ahead of a menu they already know, and an agent that finishes reading all six options first has
made them wait for nothing.
0 to escape. If the tree is genuinely deep, the honest answer is a
conversation agent that asks "what can I help with?" — which is
the thing a deep menu was approximating.What a keypad agent does not have
Set on a keypad agent, these are stored, returned, and read by nothing — so
switching back to conversation restores the agent exactly as it was:
- The prompt, the model,
temperatureandmax_tokens. knowledge_base_idsandtools.- The
conversationblock's silence and turn-taking settings — a keypad agent useskeypad_timeout_secondsandmax_no_inputinstead.
What it does read: languages (for the greeting and the voice),
keypad, the four keypad fields, transfer_number and
record_calls.