> ## Documentation Index
> Fetch the complete documentation index at: https://assemblyai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up a phone agent on your own Twilio number

> Point a Twilio number you already own at AssemblyAI, attach an agent, and take a keypad payment end to end.

Inbound only. Typed digits stay out of transcripts and logs.

**Time:** \~20 minutes.

**You need:**

* A [Twilio account](https://console.twilio.com) (Account SID + Auth Token from the Console home page)
* A phone number in that account
* An [AssemblyAI API key](https://www.assemblyai.com/app/api-keys)
* An HTTPS endpoint that accepts POST (use [webhook.site](https://webhook.site) for testing)

## 1. Install the Twilio CLI and log in

```bash Install icon=terminal theme={null}
brew tap twilio/brew && brew install twilio    # macOS
npm install -g twilio-cli                      # or anywhere with Node 14+
twilio login                                   # paste Account SID + Auth Token
```

You can do every Twilio step by clicking instead, in the [Console](https://console.twilio.com) under **Voice → Manage → Elastic SIP Trunking**.

## 2. Fill in your details

Save these in a `.env` file in your working directory:

```bash .env icon=gear theme={null}
AAI_API_KEY="your-assemblyai-api-key"

NUMBER="+15551234567"                       # your Twilio number, E.164
TRUNK_DOMAIN="my-company-agent.pstn.twilio.com"
PAYMENT_URL="https://webhook.site/your-unique-id"
```

Load it into your shell before running the commands below:

```bash theme={null}
set -a && source .env && set +a
```

`TRUNK_DOMAIN` is a name you invent. It must end in `.pstn.twilio.com` and be unique across Twilio, so include your company name.

## 3. Create the SIP trunk

A SIP trunk is the connection Twilio uses to hand calls off to AssemblyAI. You will create an empty trunk here, then configure it in the next two steps.

Create the trunk using the `$TRUNK_DOMAIN` you set in step 2:

```bash theme={null}
twilio api:trunking:v1:trunks:create \
  --friendly-name "AssemblyAI voice agent" --domain-name "$TRUNK_DOMAIN"
```

Copy the `sid` from the response and save it in your shell:

```bash theme={null}
TRUNK_SID="TKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

You will use `$TRUNK_SID` in steps 4 and 5.

## 4. Send incoming calls to AssemblyAI

```bash theme={null}
twilio api:trunking:v1:trunks:origination-urls:create \
  --trunk-sid "$TRUNK_SID" --friendly-name "AssemblyAI SIP" \
  --sip-url "sip:sip.assemblyai.com" --priority 1 --weight 1 --enabled
```

Reusing a trunk that already has an origination URL? Delete the old one first, or calls may go to the wrong place:

```bash theme={null}
twilio api:trunking:v1:trunks:origination-urls:list --trunk-sid "$TRUNK_SID"
twilio api:trunking:v1:trunks:origination-urls:remove --trunk-sid "$TRUNK_SID" --sid "OUxxxx"
```

## 5. Put your number on the trunk

Look up your number's SID:

```bash theme={null}
twilio api:core:incoming-phone-numbers:list --phone-number "$NUMBER"
```

Save it, then attach the number to the trunk:

```bash theme={null}
NUMBER_SID="PNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

twilio api:trunking:v1:trunks:phone-numbers:create \
  --trunk-sid "$TRUNK_SID" --phone-number-sid "$NUMBER_SID"
```

The trunk now controls this number. Any Voice webhook set on the number itself no longer applies.

## 6. Create your agent

Your agent has three parts:

1. A **system prompt** telling it how to behave on the call.
2. A **`process_payment` tool** that charges the card. Card number, expiry, and authorization are collected from the keypad using `dtmf_collected_arguments`, so the model never hears or sees them.
3. A **`send_receipt` tool** that emails the receipt after payment.

Save the following as `agent.json`:

```json agent.json expandable highlight={19-32} icon=file-code theme={null}
{
  "name": "payment-agent",
  "greeting": "Hello, this is Riley from Acme on a recorded line about your account.",
  "voice": { "voice_id": "eve" },
  "system_prompt": "You are an Acme agent on a recorded call about an eighty three dollar and ten cent balance. One short sentence per reply.\nAfter the greeting say: \"Our records show a balance of eighty three dollars and ten cents, would you like to settle it by card now?\"\nWhen the caller agrees, call process_payment ONCE with amount \"83.10\". The card number, expiry and authorization are entered on the phone keypad - never ask for them by voice.\nIf the result is approved, say \"Your payment is approved.\" then ask \"What email address would you like your receipt sent to?\". When they give an email, call send_receipt with that email and amount \"83.10\". Then say \"Thank you, your receipt is on its way, goodbye.\"\nIf it is not approved, apologise and say the payment was not completed. Call each tool at most once.",
  "tools": [
    {
      "name": "process_payment",
      "description": "Charge the caller's card. Card number, expiry and authorization come from the telephone keypad.",
      "http": { "url": "https://webhook.site/your-unique-id", "http_method": "POST" },
      "parameters": {
        "type": "object",
        "properties": {
          "card_number": { "type": "string", "description": "Card number from the keypad." },
          "expiry":      { "type": "string", "description": "Expiry MMYY from the keypad." },
          "amount":      { "type": "string", "description": "Amount to charge, e.g. 83.10." },
          "authorization": { "type": "string", "description": "1 authorizes the charge." }
        },
        "required": ["card_number", "expiry", "amount"]
      },
      "dtmf_collected_arguments": [
        { "parameter_name": "card_number", "min_digits": 13, "max_digits": 19,
          "sensitive": true, "terminator": "#", "confirm": true,
          "prompt": "Using your keypad, please enter your card number, then press hash." },
        { "parameter_name": "expiry", "min_digits": 4, "max_digits": 4,
          "sensitive": true, "terminator": "#", "confirm": false,
          "prompt": "Now enter the expiry as four digits, month then year." },
        { "parameter_name": "authorization", "min_digits": 1, "max_digits": 1,
          "sensitive": false, "terminator": "#", "confirm": false,
          "prompt": "Do you authorize Acme to process a one-time payment of eighty three dollars and ten cents today? Press 1 to authorize, or 2 to cancel." }
      ]
    },
    {
      "name": "send_receipt",
      "description": "Email a payment receipt after the payment is approved.",
      "http": { "url": "https://webhook.site/your-unique-id", "http_method": "POST" },
      "parameters": {
        "type": "object",
        "properties": {
          "email":  { "type": "string", "description": "Caller's email address." },
          "amount": { "type": "string", "description": "Amount charged." }
        },
        "required": ["email", "amount"]
      }
    }
  ]
}
```

Then create the agent and save its ID:

```bash Create agent icon=cloud-arrow-up wrap theme={null}
curl -X POST "https://agents.us.assemblyai.com/v1/agents" -H "Authorization: Bearer $AAI_API_KEY" \
  -H "Content-Type: application/json" -d @agent.json

AGENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # id from the response
```

### Keypad settings reference

Each entry in `dtmf_collected_arguments` controls one keypad prompt:

| Field                       | What it does                                                                                                     |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `parameter_name`            | Must match a name in `parameters`. The typed digits fill this argument.                                          |
| `prompt`                    | What the agent says before listening for digits.                                                                 |
| `min_digits` / `max_digits` | Length of the expected input. Equal values finish automatically.                                                 |
| `terminator`                | Key the caller presses to signal "done" (usually `#`). Used when length varies, e.g. card numbers.               |
| `sensitive`                 | When `true`, digits are hidden from the transcript, logs, and the model. Only your endpoint sees the real value. |
| `confirm`                   | When `true`, the agent reads the value back and asks for `1` to confirm or `2` to retry.                         |

## 7. Register the number and attach the agent

```bash Register and attach icon=phone-arrow-down-left wrap theme={null}
# 1. Register the number with AssemblyAI
curl -X POST "https://agents.us.assemblyai.com/v1/phone-numbers/import" \
  -H "Authorization: Bearer $AAI_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"phone_number\":\"$NUMBER\",\"termination_uri\":\"$TRUNK_DOMAIN\"}"

# 2. Attach the agent to the number
curl -X PUT "https://agents.us.assemblyai.com/v1/phone-numbers/$NUMBER/agent" \
  -H "Authorization: Bearer $AAI_API_KEY" -H "Content-Type: application/json" \
  -d "{\"agent_id\":\"$AGENT_ID\"}"

# 3. Verify
curl "https://agents.us.assemblyai.com/v1/phone-numbers/$NUMBER" -H "Authorization: Bearer $AAI_API_KEY"
```

`201` then `200`, and the last call should show your `agent_id` with `"type": "imported"`.

## 8. Call it

Ring the number. Say yes when it offers to take payment, then: type a card number and press `#` → press `1` to confirm → type the four digit expiry → press `1` to authorize → say your email.

Refresh [webhook.site](https://webhook.site) and you will see the payment request arrive, then the receipt request.

## If it does not work

* **Nothing happens when you call.** Check the number is on the trunk (5), the origination URL is exactly `sip:sip.assemblyai.com` and enabled (4), and an agent is attached (7). A number with a Voice webhook still set in the Console is not going through the trunk.
* **The agent asks out loud instead of using the keypad.** `parameter_name` does not match a parameter name in the tool.
* `409` number already registered · `404 agent_not_found` wrong agent ID · `422 phone_number_has_no_agent` do step 7 · `502` Twilio rejected it, retry.
* **Twilio command not recognised.** Names change between CLI versions. Run `twilio api:trunking:v1 --help`, or use the Console.
