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

# Inbound SMS webhooks

> Get a signed POST to your server the moment someone texts one of your numbers.

Register a URL once and LumisReach POSTs an `sms.received` event to it every time someone texts one of
your numbers, telling you **who texted** (their number and contact record), **which of your numbers**
they texted, and **what they said**. Works for every number on your account, whichever carrier it runs on.

## 1. Register your endpoint

```bash theme={null}
curl -X POST "https://api.lumisreach.com/api/v1/sms/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/lumisreach/sms", "phone_number_id": 12 }'
```

Leave out `phone_number_id` to hear about texts to **all** your numbers. The URL must be public HTTPS.

The response includes a `secret` (`whsec_…`). **It is shown only once**: store it now, you need it to
verify deliveries. Lost it? Delete the webhook and create a new one.

## 2. Receive `sms.received`

```http theme={null}
POST /lumisreach/sms HTTP/1.1
Content-Type: application/json
X-LumisReach-Event: sms.received
X-LumisReach-Event-Id: evt_6f1c…
X-LumisReach-Signature-256: 5d41402abc4b2a76b9719d911017c592…
```

```json theme={null}
{
  "id": "evt_6f1c2e4a-…",
  "type": "sms.received",
  "created_at": "2026-09-25T17:04:12.381Z",
  "test": false,
  "data": {
    "message_id": "6f1c2e4a-…",
    "from": "+14155550123",
    "to": "+14157279482",
    "phone_number_id": 12,
    "body": "Hey, is Thursday still good?",
    "received_at": "2026-09-25T17:04:12.102Z",
    "contact": {
      "id": "5f25da85-…",
      "name": "Jane Doe",
      "phone_number": "+14155550123",
      "email": "jane@example.com"
    }
  }
}
```

| Field                  | Meaning                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| `data.from`            | Who texted you.                                                                                |
| `data.contact`         | Their contact record. A first-time texter gets one automatically, named after their number.    |
| `data.to`              | Which of your numbers they texted.                                                             |
| `data.phone_number_id` | That number's id, as in `GET /v1/phone-numbers`.                                               |
| `data.body`            | The message.                                                                                   |
| `id`                   | Unique per text. Use it to ignore a duplicate if a retry arrives after you already handled it. |

Respond with any `2xx` within 10 seconds. To see the full thread with this person, call
`GET /v1/history/texts?phone_number=<from>`; for everything else you know about them, call
`GET /v1/contacts/<data.contact.id>`.

## 3. Verify the signature

`X-LumisReach-Signature-256` is the hex HMAC-SHA256 of the **raw request body**, keyed with your
webhook `secret`. It is the same scheme LumisReach call webhooks use.

```ts theme={null}
import { createHmac, timingSafeEqual } from "crypto";

export function isFromLumisReach(rawBody: string, signature: string, secret: string): boolean {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  );
}
```

Compute it over the body exactly as received, before any JSON parsing.

## Retries

The first delivery is made right away. If your server errors, times out, or returns a non-`2xx`
(redirects are not followed), it is retried 3 more times: after 1, 5 and 30 minutes. Every attempt is
recorded in the webhook delivery log in your dashboard.

## Test it

`POST /v1/sms/webhooks/{id}/test` sends a sample, correctly signed event with `"test": true` and tells
you how your server responded.
