---
title: "Inbound SMS"
description: "Receive replies to your numbers as verified message.received events, with sender, recipient, text, and media URLs."
---

Point the provider's inbound webhook at your server and parse each request with `parseSmsWebhook()`. Every text sent to your number becomes a `message.received` event.

## 1. Configure the inbound URL

Inbound messages go to the URL set on the receiving number or its group. A per-message `webhookUrl` only affects status events.

| Provider | Where to set it |
| --- | --- |
| Twilio | The phone number's "A message comes in" webhook, or the Messaging Service's incoming message setting. |
| Telnyx | The messaging profile's inbound webhook URL. |
| Plivo | The message URL of the Plivo application linked to the number. |
| Vonage | The Inbound URL of the Vonage application linked to the number. Send from a linked number with JWT auth. Vonage documents that Basic auth fails with 401 for linked numbers. |

## 2. Parse the message

```ts
import { parseSmsWebhook, WebhookSignatureError } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";

export async function POST(request: Request): Promise<Response> {
  try {
    const event = await parseSmsWebhook({
      provider: "plivo",
      request,
      publicUrl: "https://example.com/webhooks/sms/plivo",
      credentials: { authToken: process.env.PLIVO_AUTH_TOKEN ?? "" },
    });

    if (event.type === "message.received") {
      await db.inbox.save({ from: event.from, to: event.to, body: event.body, providerId: event.providerId });
      console.log(`Reply from ${event.from} with ${event.mediaUrls.length} attachment(s)`);
    }
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) {
      return new Response("Invalid signature", { status: 401 });
    }
    throw error;
  }
}
```

Plivo signs the URL it called, so the example passes `publicUrl`. See [Webhook security](/receiving/webhook-security#fix-url-mismatches-behind-a-proxy).

A `message.received` event has:

| Field | Value |
| --- | --- |
| `providerId` | The provider's ID for the inbound message. |
| `from` | The sender's number. Vonage numbers, which arrive without `+`, are normalized to E.164. |
| `to` | Your number that received it. |
| `body` | The text, as sent. |
| `mediaUrls` | Attached media from Twilio `MediaUrl0…`, Telnyx `media[].url`, or Plivo `Media0…`. Always empty for Vonage. |
| `dedupeKey`, `raw`, `occurredAt` | See [Webhook events](/reference/webhook-events). |

## Keywords become opt-out events

When the whole message is a keyword such as `STOP`, `UNSUBSCRIBE`, or `HELP`, you get `recipient.opted_out` or `recipient.help` instead of `message.received`. Handle those too, or turn detection off with `detectKeywords: false`. See [STOP, HELP, and opt-outs](/receiving/stop-help-and-opt-outs).

## Reply to a message

SMS SDK never replies automatically. To answer, send a new message from the number that was texted:

```ts
import { isE164 } from "@opencoredev/sms-sdk";
import { parseSmsWebhook } from "@opencoredev/sms-sdk/webhooks";
import { sms } from "./sms";

export async function POST(request: Request): Promise<Response> {
  const event = await parseSmsWebhook({
    provider: "telnyx",
    request,
    credentials: { publicKey: process.env.TELNYX_PUBLIC_KEY ?? "" },
  });

  if (event.type === "message.received" && isE164(event.from) && isE164(event.to)) {
    await sms.send({
      to: event.from,
      from: event.to,
      body: "Thanks, we got your message.",
      idempotencyKey: `reply:${event.dedupeKey}`,
    });
  }
  return new Response(null, { status: 200 });
}
```

Deriving the idempotency key from the inbound `dedupeKey` means a repeated webhook cannot send a second reply, as long as the client has an [idempotency store](/sending/idempotency). Catch `WebhookSignatureError` here as in the first example.

Twilio also lets you reply by returning TwiML from the webhook. SMS SDK does not build TwiML. If you do not want Twilio to reply, return an empty `<Response></Response>` document with `Content-Type: text/xml`.

## Limitations

- SMS SDK is not a conversation store. It does not thread messages, keep history, or manage an inbox.
- Inbound MMS media URLs point at provider storage. They may expire, and fetching them may need provider credentials.
- Vonage inbound requires a number linked to a Vonage application. See [Vonage](/providers/vonage).

## Next steps

- [STOP, HELP, and opt-outs](/receiving/stop-help-and-opt-outs)
- [Webhook security](/receiving/webhook-security)
