---
title: "SMS"
source: https://sumgenius.ai/docs/webhook-sms
generated: 2026-09-05
---

# SMS

Send and receive SMS through the same API used for every other channel. This page covers only what is specific to SMS: phone-number routing, encoding and segment math, quota rules, opt-out, and the SMS error reference. Everything shared lives on the [authentication](https://sumgenius.ai/docs/api-authentication), [receiving webhooks](https://sumgenius.ai/docs/receiving-webhooks), and [Send API](https://sumgenius.ai/docs/send-api) pages.

Add-on · $29/month · Requires Professional+ plan

### Overview

Inbound texts to your provisioned number arrive as `sms.message_received` webhook events; outbound goes through the same Send API as DMs, routed by `to_phone`. The envelope, signing, retries, and API keys are identical to every other channel: see [the envelope](https://sumgenius.ai/docs/webhook-events#envelope), [verifying signatures](https://sumgenius.ai/docs/receiving-webhooks#verify), and [authentication](https://sumgenius.ai/docs/api-authentication).

Channel rules

- send_messageonly action `react`, `unreact`, and `typing_on` are Meta-only and are rejected on an SMS conversation.
- inboundfree Never counted toward your monthly SMS quota, regardless of segments.
- outbound1 quota unit per call Each successful send counts as 1 toward the monthly SMS quota, no matter how many segments Twilio splits the message into.

> **Note**
>
> **Requirements:** SMS access on your tier (Professional or Business) and a provisioned Twilio number under **Settings → SMS**. Without a provisioned number, the `sms.message_received` checkbox on the endpoint page is disabled.

To handle SMS conversations yourself with no AI reply, disable the SMS AI Assistant under *Settings → SMS* or use [Global External Reply Mode](https://sumgenius.ai/docs/receiving-webhooks#external-reply-mode); to keep message bodies out of stored delivery logs, see [Inbound Body Purge](https://sumgenius.ai/docs/receiving-webhooks#inbound-body-purge).

**At a glance**

```
// inbound event
event_type: "sms.message_received"
channel:    "twilio_sms"

// outbound
action:  send_message only
routing: to_phone (E.164) or conversation_id
quota:   inbound free, outbound 1 per call
```

### Inbound payload

A customer text arrives as an `sms.message_received` event: the standard envelope plus an SMS sub-block under `message.sms`. `source.channel` is always `twilio_sms`.

SMS-specific fields

- customer.platform_user_idstring The customer's phone number in E.164 format. Always present. `customer.phone` repeats it as a convenience (`full` payload profile only).
- message.idstring The Twilio MessageSid. Stable identifier; use it for deduplication.
- message.text_rawstring The full SMS body. Multi-segment SMS arrives concatenated: you receive the joined text, not individual segments.
- message.sms.to_phone · from_phonestring Your ChatGenius (Twilio) number that received the SMS, and the customer's number, both E.164.
- message.sms.num_segmentsinteger How many segments the inbound message used. Informational only.
- message.sms.num_mediainteger The media count Twilio reported. Forwarded for visibility, but MMS media URLs are **not** included in v1: `message.attachments` is always an empty array for SMS.
- message.sms.encodingenum `GSM-7` or `UCS-2`, detected from the message body. See [encoding & segments](#encoding).

Signature verification, retries, and timestamp validation are identical to every other channel: see [verifying signatures](https://sumgenius.ai/docs/receiving-webhooks#verify).

**sms.message_received · full profile**

```
{
  "event_id":    "7f7a80c84bf17c4b5b72974c97365a77",
  "event_type":  "sms.message_received",
  "occurred_at": "2026-04-27T23:22:10Z",
  "source": {
    "platform": "sms",
    "channel":  "twilio_sms"
  },
  "conversation": {
    "id": 119301
  },
  "customer": {
    "platform_user_id": "+15551234567",
    "phone":            "+15551234567"
  },
  "message": {
    "id":          "SMdb4342728f73bca6a83770a705e27b31",
    "text_raw":    "Hi, what time do you open Saturday?",
    "attachments": [],
    "sms": {
      "to_phone":     "+15559876543",
      "from_phone":   "+15551234567",
      "num_segments": 1,
      "num_media":    0,
      "encoding":     "GSM-7"
    }
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-04-27T23:22:10Z"
  }
}
```

### POST Sending SMS

Same endpoint and body as every other channel; the only SMS-specific routing field is `to_phone`. Shared fields (`idempotency_key`, response shape, generic errors) are documented on the Send API page: [send_message](https://sumgenius.ai/docs/send-api#send-message), [idempotency](https://sumgenius.ai/docs/send-api#idempotency), [response format](https://sumgenius.ai/docs/send-api#response-format), [errors](https://sumgenius.ai/docs/send-api#errors).

SMS routing & body

- to_phonestring Recipient phone in E.164 format (e.g. `+15551234567`). Required if `conversation_id` is not provided; the platform auto-sets to `sms`. Numbers are auto-normalized: spaces, dashes, parens, and a leading `1` without `+` are accepted. A number with no existing conversation auto-creates one (cold outbound).
- conversation_idinteger The `conversation.id` from a received SMS event. Use this or `to_phone`.
- message_textstringrequired The SMS body. Max 1,600 chars (GSM-7) or 1,530 chars (UCS-2). See [encoding & segments](#encoding).
- from_phonenot accepted The sender is always your provisioned `sms_phone_number`; sender selection is server-controlled to enforce A2P 10DLC compliance.

The success response follows the standard [shape](https://sumgenius.ai/docs/send-api#response-format) with `platform: "sms"`, plus `to_phone` echoed back when it was sent in the request.

**Endpoint**

```
POST https://sumgenius.ai/api/meta/webhook-send.php
```

**Request**

```
curl -X POST https://sumgenius.ai/api/meta/webhook-send.php \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "refill-reminder-001",
    "action":          "send_message",
    "to_phone":        "+15551234567",
    "message_text":    "Your prescription is ready for pickup. Reply STOP to opt out."
  }'
```

Examples on this page use the [Send API](https://sumgenius.ai/docs/send-api). New integrations should prefer [`POST /v1/messages`](https://sumgenius.ai/docs/rest-api#send-message), which accepts the same body and returns the v1 envelope.

### Encoding & segments

SMS uses two encodings depending on the characters in the body. ChatGenius detects the encoding automatically and validates against the corresponding cap.

- GSM-7160 / segment · cap 1,600 Latin alphabet, common punctuation, basic European accents. Concatenated messages drop to 153 chars per segment.
- UCS-270 / segment · cap 1,530 Triggered by any emoji, non-Latin script (Arabic, Chinese, etc.), or smart quotes. Concatenated messages drop to 67 chars per segment.

> **Warning**
>
> **One emoji can drop your character limit by more than half.** A 160-character GSM-7 message that fit in 1 segment becomes a 70-char-per-segment UCS-2 message the moment a single emoji is added: same text, now 3 segments.

Quota counting

- inboundfree Never counted, regardless of segments.
- outbound1 per call Each successful send counts as 1 quota unit regardless of segment count. A 200-char GSM-7 message is 2 Twilio segments but 1 quota unit; a 200-char UCS-2 message is 3 segments, still 1 quota unit.

> **Note**
>
> Segment count still matters for **Twilio's carrier-level throughput limits**: sending many long messages quickly can hit Twilio's per-second segment cap before it hits our monthly quota. Keep messages concise where possible.

**Segment math**

```
// GSM-7: 160 chars = 1 segment, then 153 per segment
// UCS-2:  70 chars = 1 segment, then 67 per segment

"Your table is ready!"      // GSM-7, 1 segment
"Your table is ready! 🎉"   // UCS-2 now, still 1 segment

// 200-char GSM-7 body: 2 segments, 1 quota unit
// 200-char UCS-2 body: 3 segments, 1 quota unit
```

### Opt-out (STOP / START)

SMS opt-out is handled automatically at multiple layers; you don't implement any of this.

- Twilio carrier-level Advanced Opt-Out When a recipient texts `STOP`, Twilio blocks all future outbound SMS to that number from your Messaging Service, regardless of any application-layer check.
- ChatGenius app-level fast-fail The opt-out is mirrored, so future API calls fail immediately with `status: "dead_letter"` and never consume a Twilio API call.
- Re-subscription If the recipient texts `START`, `UNSTOP`, or `YES`, both layers re-enable sending automatically.

You should still include opt-out language in your initial messages (e.g. *"Reply STOP to opt out."*) to comply with carrier and CASL/TCPA requirements.

**Send to an opted-out number · HTTP 422**

```
{
  "success": false,
  "status":  "dead_letter",
  "error":   "Recipient has opted out (replied STOP)."
}
```

### SMS error reference

SMS errors follow the same [general error response shape](https://sumgenius.ai/docs/send-api#errors). The codes specific to SMS are:

**Branch on `error`, not on the status code.** Two validations run before the send is queued and return their own status. Everything after that point is queued first so the failure lands in your Send Logs, and those all come back as `422` with `status: "dead_letter"` and the specific reason in `error`.

| HTTP | Trigger | Error message |
| --- | --- | --- |
| 400 | `to_phone` doesn't match E.164 | `to_phone must be in E.164 format (e.g. +15551234567).` |
| 400 422 | The recipient is E.164-shaped but not a real US or Canadian number. Either way you get this back on the send call rather than as a delivery failure later. Routing by `to_phone` is checked before the message is queued and returns `400`; routing by `platform_user_id` or `conversation_id` is checked after queueing so it shows in your Send Logs, returns `422`, and the message names the conversation and the stored number. | `to_phone uses area code 299, which is not assigned in the North American Numbering Plan.` `to_phone is not a valid North American number: area code 040 and exchange 484 must each start with a digit 2-9.` `to_phone has 13 digits after the country code; a US or Canadian number needs exactly 10 (e.g. +15551234567).` `The recipient number on conversation 192883 (+12998346713) uses area code 299, which is not assigned in the North American Numbering Plan.` |
| 422 | SMS not enabled on your account tier | `SMS access is not enabled for this account.` |
| 422 | Recipient has opted out (replied STOP) | `Recipient has opted out (replied STOP).` |
| 422 | The recipient is a landline. A carrier already rejected an earlier message to this number, so we refuse it up front rather than pay for the same rejection again. The matching `message.send_failed` event carries `error_code: 30006`, the same code the carrier itself returns, so you can handle both the same way. | `Recipient is a landline or a carrier that cannot receive SMS. The carrier rejected an earlier message to this number.` |
| 422 | No Twilio number provisioned for the account | `SMS not provisioned for this account. Provision a Twilio number first.` |
| 422 | Message exceeds the GSM-7 (1,600) or UCS-2 (1,530) cap, or an unsupported action was used | `message_text exceeds 1600 characters for SMS GSM-7 (1700 provided).` |
| 422 | Monthly SMS quota exceeded | `Monthly SMS limit reached for this account.` |
| 422 | Twilio returned a permanent error (invalid number, region not enabled, etc.). Transient Twilio failures are retried instead and come back as `200` with `status: "queued_retry"`. | `SMS delivery failed: <Twilio error message>.` |
| 422 | SMS add-on is temporarily disabled platform-wide | `SMS webhook add-on is temporarily disabled.` |

Permanent failures (invalid recipient, opted out, landline, over-length) are not retried; they go straight to `dead_letter`. Transient failures (Twilio 5xx, 429, network errors) are retried with the same backoff schedule used for Meta deliveries.

**Over-length body · HTTP 422**

```
{
  "success": false,
  "status":  "error",
  "error":   "message_text exceeds 1600 characters for SMS GSM-7 (1700 provided)."
}
```
