# 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

`PATCH /v1/agents/{id}`

**Shell**

```bash
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 greeting *is* the menu.** There is nothing else to read the options out, so an agent in keypad mode with a one-line greeting has a menu nobody can use.

### The entries

| Field | Type | Description |
| --- | --- | --- |
| `keypad` optional | 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. |
| `say` optional | string | Spoken back when that key is pressed. Supports `{{variables}}`. |
| `end_call` optional | boolean | Hangs up once `say` has been spoken in full — not the instant the line ends, so the caller hears all of it. |
| `transfer` optional | 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_to` optional | string | This key's own destination, E.164. Lets 1 and 2 reach different teams. |
| `no_match` optional | string | Spoken when the caller presses something with no entry. **Three in a row ends the call.** |
| `no_input` optional | string | Spoken when nothing is pressed — usually the menu again. |
| `max_no_input` optional | integer | How many times to re-read the menu to a silent caller before giving up. The call ends `no_input`. |
| `keypad_timeout_seconds` optional | 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.

**JSON**

```json
{
  "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](https://voice.sphoro.com/docs/transfers).

## 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.

`GET /v1/calls/{id}/events`

**Shell**

```bash
curl -sN https://voice.sphoro.com/v1/calls/$CALL_ID/events \
  -H "Authorization: Bearer $SPHORO_API_KEY" | grep dtmf
```

### On 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.

> **Keep menus to four options and one level.** Callers do not hold six options in their head, and a second level of menu is where people start pressing `0` to escape. If the tree is genuinely deep, the honest answer is a [conversation agent](https://voice.sphoro.com/docs/agent-types) 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, `temperature` and `max_tokens`.
- `knowledge_base_ids` and `tools`.
- The `conversation` block's silence and turn-taking settings — a keypad agent uses `keypad_timeout_seconds` and `max_no_input` instead.

What it *does* read: `languages` (for the greeting and the voice), `keypad`, the four keypad fields, `transfer_number` and `record_calls`.
