Receiving overview
Turn provider webhooks into verified, normalized SmsEvent objects for delivery status, inbound messages, and opt-outs.
Providers report delivery status and incoming texts by calling a webhook on your server. Each provider has its own payload and signature. parseSmsWebhook() verifies the request and returns one SmsEvent shape for all four.
A successful send() only means the provider accepted the message. Whether it reached the phone arrives later as a status webhook. See Delivery status webhooks.
How it works
- You set a webhook URL in the provider’s console, or pass
webhookUrlper message. - The provider calls that URL when a status changes or a message arrives.
- Your handler passes the
Request, provider name, and credentials toparseSmsWebhook(). - SMS SDK checks the signature over the raw body before reading any field.
- You get an
SmsEvent, or aWebhookSignatureErrorif the request is not authentic.
A complete handler
import { parseSmsWebhook, WebhookSignatureError, type SmsEvent } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";
export async function handleTelnyxWebhook(request: Request): Promise<Response> {
let event: SmsEvent;
try {
event = await parseSmsWebhook({
provider: "telnyx",
request,
credentials: { publicKey: process.env.TELNYX_PUBLIC_KEY ?? "" },
});
} catch (error) {
if (error instanceof WebhookSignatureError) {
return new Response("Invalid signature", { status: 401 });
}
throw error;
}
if (!(await db.webhookEvents.insertIfNew(event.dedupeKey))) {
return new Response(null, { status: 200 }); // already handled
}
switch (event.type) {
case "message.delivered":
case "message.undelivered":
case "message.filtered":
await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
break;
case "message.received":
await db.inbox.save({ from: event.from, to: event.to, body: event.body, providerId: event.providerId });
break;
case "recipient.opted_out":
await db.suppressions.add(event.from);
break;
}
return new Response(null, { status: 200 });
}
The handler answers 401 to forged requests, acknowledges repeats without reprocessing them, and records the events it cares about. Framework versions are in Next.js and Hono and Bun.
Event types
type |
When |
|---|---|
message.queued, message.sent, message.delivered, message.undelivered, message.filtered |
A status update for a message you sent. See Delivery status webhooks. |
message.received |
Someone texted your number. See Inbound SMS. |
recipient.opted_out, recipient.opted_in, recipient.help |
An inbound STOP, START, or HELP. See STOP, HELP, and opt-outs. |
unrecognized |
A verified event SMS SDK does not map, such as a new status. raw holds the payload. |
Every event has provider, dedupeKey, and raw, plus occurredAt when the payload includes a time. The full shapes are in Webhook events.
Choose a page
| You want to | Page |
|---|---|
| Track delivery of sent messages | Delivery status webhooks |
| Read replies | Inbound SMS |
| Honor STOP and answer HELP | STOP, HELP, and opt-outs |
| Fix signature failures behind a proxy | Webhook security |
| Handle repeats and out-of-order events | Duplicates and ordering |
Best practices
- Verify every request. Never set
unsafeSkipVerificationoutside tests. See Webhook security. - Respond fast. Store the event and do slow work in a background job. Providers retry webhooks that time out, which creates duplicates.
- Deduplicate on
dedupeKey, and do not trust order. Amessage.sentcan arrive aftermessage.delivered. See Duplicates and ordering.