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

# Twilio (SMS and WhatsApp)

> Send texts and WhatsApp messages from a workflow, and start a workflow when one of your numbers receives one

The Twilio integration lets a workflow or an agent **send** SMS, MMS and WhatsApp messages through your own Twilio account, read message history, and **start a run** when one of your Twilio numbers (or WhatsApp senders) receives a message.

## Connect your Twilio account

In **Settings → Connections**, add a Twilio connection with:

| Field | Where to find it |
| - | - |
| **Account SID** | The Twilio Console dashboard (`AC…`). A subaccount connects with its own SID. |
| **Auth Token** | Next to the Account SID. Use the account's **primary** Auth Token. |

Reliant checks the pair with Twilio before saving, so a typo is caught right away. The connection is labelled with the account's name in Twilio.

<Note>
  Twilio connects with the Account SID and Auth Token, not an API key. Twilio signs every incoming-message webhook with the account's primary Auth Token, and an API key cannot verify those signatures. If you rotate the Auth Token in Twilio, reconnect with the new one: until you do, sends fail with error 20003 and incoming messages for that connection stop arriving.
</Note>

## Sending messages

The `twilio/message.send@1` action (also offered to agents as a tool) sends from one of your numbers:

```yaml theme={null}
- id: notify
  type: action
  uses: twilio/message.send@1
  with:
    to: "+15551234567"
    from: "+15559870000"          # one of your Twilio numbers, or use messaging_service_sid
    body: "Deploy {{ nodes.deploy.version }} finished"
```

* **SMS / MMS:** E.164 numbers (`+15551234567`). Attach media with `media_url` (a list of public URLs).
* **WhatsApp:** prefix both `to` and `from` with `whatsapp:` (`whatsapp:+15551234567`).
* **Outside WhatsApp's 24-hour window:** WhatsApp only allows free-form text within 24 hours of the person's last message to you. Outside that window, send an approved template with `content_sid` (and `content_variables`) instead of `body`. A free-form send outside the window fails with error **63016**, and the error message says so.

The other actions are `message.get` (a message's delivery status), `message.list` (history, filtered by `to`, `from` or date) and `phone_number.list` (your numbers and where each one's incoming messages go today).

Errors your workflow can act on are classified for you:

| Twilio code | Meaning | Retryable |
| - | - | - |
| 20003 | Wrong or rotated Auth Token: reconnect Twilio | No |
| 21211, 21614 | The `to` number is invalid or not a mobile number | No |
| 21608 | Trial account (or no approved compliance profile): only verified numbers can be reached | No |
| 63016 | WhatsApp outside the 24-hour window: use a template | No |
| 63015 | The recipient has not joined your WhatsApp Sandbox | No |
| 21610 | The recipient replied STOP | No |
| 429 / 20429 | Rate limited | Yes |

## Starting a run when a message arrives

Add a trigger with the **Message received** event (`twilio/message.received@1`). In workflow YAML:

```yaml theme={null}
triggers:
  - name: support-texts
    integration:
      integration: twilio
      events: [message.received]
      match: { to: "+15559870000" }           # optional: only this number
    filter: "!trigger.payload.data.body.contains('STOP')"
```

The run sees the message as `trigger.payload`:

| Path | Value |
| - | - |
| `trigger.payload.data.body` | The message text |
| `trigger.payload.data.from` / `.to` | The sender, and your number that received it (`whatsapp:+…` for WhatsApp) |
| `trigger.payload.data.media` | `[{url, content_type}]` for attached media |
| `trigger.payload.data.profile_name` | The sender's WhatsApp profile name (WhatsApp only) |
| `trigger.payload.data.message_sid` | The message SID, for `twilio/message.get` |
| `trigger.payload.attributes.channel` | `sms` (including MMS) or `whatsapp` |

A trigger can be narrowed by equality on these attributes:

| Attribute | Example | Notes |
| - | - | - |
| `to` | `+15559870000` | Your number or sender that received it. Use this to listen to one number. |
| `from` | `+15551230000` | The sender. |
| `channel` | `sms` / `whatsapp` | Derived from the `whatsapp:` prefix. |
| `num_media` | `"0"` | How many media items, as a string. |

To reply, use `twilio/message.send@1` with `to` set to the incoming `from` and `from` set to the incoming `to`.

### Point your numbers at Reliant

Twilio has no app-wide webhook. Each phone number decides where its incoming messages go, so you set it once per number:

1. Open the trigger in Reliant and copy its **Webhook URL**. It is the same for every number and every trigger: `https://<your Reliant host>/integrations/twilio/events`.
2. In the Twilio Console, open **Phone Numbers → Manage → Active numbers**, pick the number, and under **Messaging configuration** set **A message comes in** to **Webhook**, the URL from step 1, **HTTP POST**. Save.
3. For a **Messaging Service**, set the same URL under the service's **Integration → Incoming Messages → Send a webhook**.
4. For **WhatsApp**:
   * **Sandbox** (Messaging → Try it out → Send a WhatsApp message): set **When a message comes in** to the URL. Each tester first sends `join <your keyword>` to the sandbox number, and sandbox sessions expire after three days. The sandbox can only message people who have joined it (error 63015 otherwise).
   * **Approved sender** (WhatsApp Senders, via Self Sign-up): set the sender's incoming-message webhook to the URL. This needs a Meta Business Manager account, a display name Meta has approved, and Meta-approved templates for anything outside the 24-hour window.

Reliant answers each message with empty TwiML, so Twilio sends nothing back to the texter on its own. Any reply is whatever your workflow sends.

### How incoming messages are verified and routed

Every message Twilio sends to Reliant is signed with the account's Auth Token. Reliant checks the signature against the Auth Token saved in **each Twilio connection for that account**, and a message reaches a trigger only through a connection whose own token verified it. So:

* People on **different Twilio accounts** never see each other's messages, even though their numbers all point at the same URL.
* Several people **on the same Twilio account** each receive the message, through their own connection, wherever their trigger matches it. Use `to` to give each person their own number.
* A connection with an **outdated Auth Token** receives nothing until it is reconnected.
* A message that arrives more than once (Twilio retries) starts at most one run: messages are deduplicated on their `MessageSid`.

## For operators

The Twilio integration needs no app registration and no deployment secret. It needs only:

* **`PUBLIC_URL`** set to the externally reachable https base of the api-server. Twilio signs the exact URL it was configured with, so Reliant rebuilds that URL from `PUBLIC_URL` rather than from the request it receives behind the ingress. Without `PUBLIC_URL`, Twilio triggers are not offered.
* The `/integrations/twilio/events` path routed to the api-server, publicly reachable over https with a certificate Twilio trusts (Twilio refuses self-signed certificates).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.