# SMS webhooks

Texts send webhooks like emails do: subscribe an endpoint to the events you want, and we sign each delivery the same way.

## The events

| Event | When | data |
| --- | --- | --- |
| `sms.queued` | We accepted the text, or scheduled it. | The text |
| `sms.sent` | It was handed to the carrier network. | The text |
| `sms.delivered` | The phone received it. | The text |
| `sms.failed` | It could not be delivered, or we skipped it. | The text, plus failed.type and failed.reason |
| `sms.received` | Someone texted one of your numbers. | The received text |
| `contact.sms_unsubscribed` | A phone opted out, by STOP or any other way. | contact_id, phone, origin, occurred_at |

Add them to an endpoint's `events` with `POST /webhooks` or `PATCH /webhooks/{webhook_id}`. A text from a campaign or an automation carries `broadcast_id` or `automation_run_id`. A canceled text sends nothing.

## A delivered text

sms.delivered:

```text
{
  "type": "sms.delivered",
  "created_at": "2026-10-19T20:41:04.000Z",
  "data": {
    "object": "sms",
    "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a",
    "status": "delivered",
    "from": "+18335550199",
    "to": "+14155550123",
    "text": "Acme: Your order #1042 has shipped.",
    "category": "transactional",
    "encoding": "gsm7",
    "segments": 1,
    "price_each": "0.015",
    "cost": "0.015",
    "currency": "usd",
    "included": true,
    "scheduled_at": null,
    "created_at": "2026-10-19T20:41:00.000Z",
    "tags": {
      "order": "1042"
    }
  }
}
```

## A reply

sms.received:

```text
{
  "type": "sms.received",
  "created_at": "2026-10-19T20:41:04.000Z",
  "data": {
    "object": "received_sms",
    "id": "01928c3f-0c1d-7e2f-8a3b-4c5d6e7f8091",
    "from": "+14155550123",
    "to": "+18335550199",
    "text": "Is my order on its way?",
    "media": [],
    "keyword": null,
    "action": "forwarded",
    "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
    "received_at": "2026-10-19T20:45:00.000Z"
  }
}
```

STOP, START, HELP and Y replies do not send `sms.received`: we act on them, and an opt-out sends `contact.sms_unsubscribed`. Images in a reply come as short-lived signed links.
