---
title: "Delivery status webhooks"
description: "Learn whether an accepted message was delivered: configure status callbacks and map each provider's statuses to SmsEvent types."
---

A successful `send()` means the provider accepted the message. It does not mean the phone received it. The result's `handoff` is always `"accepted"`, and `delivery` holds the status the provider reported at that moment, usually `queued`. Delivery arrives later as a status webhook. `parseSmsWebhook()` turns it into a `message.*` event that you match to the send by `providerId`.

## 1. Point the provider at your endpoint

Set the status URL per message or in the provider's console:

| Provider | Where the status URL comes from |
| --- | --- |
| Twilio | `webhookUrl` on the message (sent as `StatusCallback`), or the Messaging Service's status callback. |
| Telnyx | `webhookUrl` on the message, or the messaging profile's webhook URL. |
| Plivo | `webhookUrl` on the message (sent as `url` with `method: "POST"`). |
| Vonage | `webhookUrl` on the message, or the Vonage application's Status URL. Requires JWT auth. |

Per message:

```ts
import { sms } from "./sms";

const result = await sms.send({
  to: "+14155550123",
  body: "Your order has shipped.",
  webhookUrl: "https://example.com/webhooks/sms/twilio",
});
```

Save `result.providerId` with the message. Status events carry the same ID.

## 2. Parse the status event

```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: "twilio",
      request,
      publicUrl: "https://example.com/webhooks/sms/twilio",
      credentials: { authToken: process.env.TWILIO_AUTH_TOKEN ?? "" },
    });

    if (
      event.type === "message.queued" ||
      event.type === "message.sent" ||
      event.type === "message.delivered" ||
      event.type === "message.undelivered" ||
      event.type === "message.filtered"
    ) {
      await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
    }
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) {
      return new Response("Invalid signature", { status: 401 });
    }
    throw error;
  }
}
```

Each status event has `providerId` and `providerStatus`, the provider's own status word such as `undelivered`. It also has `errorCode`, `from`, and `to` when the provider sent them. Twilio and Plivo sign the URL they called, which is why the example passes `publicUrl`. See [Webhook security](/receiving/webhook-security#fix-url-mismatches-behind-a-proxy).

## Status mapping

| Event | Twilio `MessageStatus` | Telnyx `to[0].status` | Plivo `Status` | Vonage `status` |
| --- | --- | --- | --- | --- |
| `message.queued` | queued, accepted, scheduled, sending | queued, sending | queued | (none) |
| `message.sent` | sent | sent, delivery_unconfirmed | sent | submitted |
| `message.delivered` | delivered | delivered | delivered | delivered |
| `message.undelivered` | undelivered, failed | sending_failed, delivery_failed, expired | undelivered, failed | rejected, undeliverable |
| `message.filtered` | undelivered or failed with `ErrorCode` 30007 | a failure with error code 40002, 40003, or 40322 | never | rejected or undeliverable with error 1210, 1470, 1472, or 1480 to 1483 |

Telnyx status events come from its `message.sent` and `message.finalized` webhooks.

Any other status, such as Twilio `read` or `canceled` or Plivo `read`, becomes `{ type: "unrecognized", providerEventType }` instead of throwing. A provider adding a status does not break your handler.

`message.filtered` requires an error code that marks the message as spam or blocked content. A plain failure stays `message.undelivered`. Plivo has no documented filter signal, so its failures are always `message.undelivered`.

## What "delivered" means

`delivered` means the carrier reported delivery to the handset. Not every carrier reports it. Some routes stop at `sent`, and Telnyx reports `delivery_unconfirmed`, mapped to `message.sent`, when the carrier gives no receipt. Treat `message.sent` with no later event as probably delivered but unconfirmed.

## Limitations

- Status webhooks repeat and arrive out of order. See [Duplicates and ordering](/receiving/duplicates-and-ordering).
- Vonage with Basic auth (`apiKey` and `apiSecret`) does not send status webhooks. Use JWT auth.
- When you switch providers, configure status URLs and webhook credentials on the new one too.
- SMS SDK does not poll for status. If a webhook never arrives, look the message up with the provider.

## Next steps

- [Inbound SMS](/receiving/inbound-sms)
- [Webhook events reference](/reference/webhook-events)
