# SMS with Twilio

> Send Ray notifications as SMS through your own Twilio account. Account SID and Auth Token, sender numbers and Messaging Services, E.164 recipients, pacing and errors.

Source: https://ray.gege.mn/docs/channels/sms-twilio

The `twilio_sms` channel sends text messages with Twilio's Messages API from your own Twilio account. Each send targets one phone number in international E.164 form. API calls go to `https://ray-api.gege.mn` with `Authorization: Bearer $RAY_API_KEY`.

| | |
| --- | --- |
| Channel type | `twilio_sms` |
| Template kind | `sms_text` (shared with [sendsms.mn](https://ray.gege.mn/docs/channels/sms-sendsms-mn.md)) |
| Recipient | `{ "phoneNumber": "+97699112233" }` |
| Pacing | `ratePerMinute`, default 300, max 1600 |
| Suppression | None (Twilio itself blocks numbers that replied STOP) |

## What you need from Twilio

1. In the [Twilio Console](https://console.twilio.com), copy the **Account SID** (starts with `AC`) and **Auth Token** from the account dashboard.
2. Choose a sender:
   - a Twilio phone number that can send SMS, in E.164 form such as `+15017122661`;
   - an alphanumeric sender id such as `ACME` (1 to 11 letters, digits or spaces), in countries that allow it;
   - or a **Messaging Service** (SID starts with `MG`), which picks a sender from its pool.
3. Under **Messaging → Settings → Geo permissions**, enable every country you will send to.

## Channel config

Create the config in the dashboard: **Channels**, **New channel**, **SMS (Twilio)**. The Auth Token is stored encrypted and never shown again.

| Field | Required | Notes |
| --- | --- | --- |
| Name | yes | Internal label, for example `otp-sms`. |
| Account SID (`accountSid`) | yes | `AC` followed by 32 hex characters. |
| Auth Token (`authToken`) | yes | When editing, leave blank to keep the stored token. Changing the Account SID requires a new token. |
| From (`from`) | one of these two | Sender number in E.164 or an alphanumeric sender id. |
| Messaging Service SID (`messagingServiceSid`) | one of these two | `MG` followed by 32 hex characters. Set either this or From, not both. |
| Rate limit, messages per minute (`ratePerMinute`) | no | Default 300, max 1600. |

Find the config's id with `GET /channels`; its `kind` is `twilio_sms`.

## Template content

| Field | Type | Notes |
| --- | --- | --- |
| `text` | string, 1 to 1600 | Plain text. Twilio splits long messages into segments (about 160 GSM-7 characters, or 70 when the text has Cyrillic or emoji), and each segment is billed as one SMS. |

Param values are inserted verbatim; SMS has no markup to escape. The same `sms_text` template also works with a sendsms.mn channel, which splits long messages into SMS of at most 159 plain Latin or 69 Cyrillic characters. Text longer than 1600 characters after params are filled in is rejected with `400`.

## Recipient

```json
{ "phoneNumber": "+97699112233" }
```

| Field | Type | Notes |
| --- | --- | --- |
| `phoneNumber` | string | Required. International number: `+`, the country code, then the number. Spaces, dashes, dots and parentheses are removed, and a leading `00` is read as `+`. A number without a country code is rejected with `400`, because Ray can't tell which country it belongs to. |

## Send a message

```bash
curl -X POST https://ray-api.gege.mn/send \
  -H "Authorization: Bearer $RAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: otp-8f2c" \
  -d '{
    "channelConfigId": "8e9f0a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b",
    "content": { "text": "Your Acme code is {{code}}. It expires in 10 minutes." },
    "params": { "code": "482913" },
    "logTitle": "Login code",
    "logDescription": "Login code sent by SMS",
    "recipient": { "phoneNumber": "+97699112233" }
  }'
```

The delivery row's `providerMessageId` is Twilio's message SID (`SM…`). `delivered` means Twilio accepted and queued the message; Ray doesn't subscribe to Twilio's status callbacks, so carrier delivery is visible in the Twilio Console's messaging logs.

## Pacing

Ray paces each Twilio account at `ratePerMinute` (default 300). Configs on the same Account SID share one pace. Twilio queues messages beyond a sender's own throughput (about one message per second for a standard long code), so a burst still goes out in order, only more slowly.

## Errors

Errors appear on the delivery row as `providerError: { "name": "TwilioError", "message": "Twilio send failed: <status> (code <twilio code>) <message>" }`.

| Result | Cases |
| --- | --- |
| `failed_terminal` (not retried) | Any 4xx except 429, for example `21211` invalid `To` number, `21408` country not enabled in geo permissions, `21610` recipient replied STOP, `21606` the From number can't send SMS, `20003` wrong Account SID or Auth Token |
| `failed_retryable` (retried) | `429` (`20429` too many requests), 5xx, network errors and timeouts |

Request errors from `POST /send` are listed in [Errors](https://ray.gege.mn/docs/errors.md).