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

# 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 return `410` immediately without consuming 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 410**

```
{
  "success": false,
  "status":  "error",
  "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:

| HTTP | Trigger | Error message |
| --- | --- | --- |
| 400 | `to_phone` doesn't match E.164 | `to_phone must be in E.164 format (e.g. +15551234567).` |
| 400 | The recipient is E.164-shaped but not a real US or Canadian number. Checked before the message is queued, so you get this back on the send call rather than a delivery failure later. Applies whether you route by `to_phone`, `platform_user_id` or `conversation_id`; when you route by conversation 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.` |
| 403 | SMS not enabled on your account tier | `SMS access is not enabled for this account.` |
| 410 | Recipient has opted out (replied STOP) | `Recipient has opted out (replied STOP).` |
| 412 | 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).` |
| 429 | Monthly SMS quota exceeded | `Monthly SMS limit reached for this account.` |
| 502 | Twilio returned an error (network, invalid number, landline, region not enabled, etc.) | `SMS delivery failed: <Twilio error message>.` |
| 503 | SMS add-on is temporarily disabled platform-wide | `SMS webhook add-on is temporarily disabled.` |

Permanent failures (invalid recipient, opted out, 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)."
}
```
