Duplicates and ordering
Providers deliver webhooks more than once and out of order. Deduplicate with dedupeKey and never move a message's status backward.
Providers retry webhooks that fail or time out, and they send events in parallel. The same event can arrive twice, and message.sent can arrive after message.delivered. Your handler has to tolerate both.
Deduplicate with dedupeKey
Every SmsEvent has a dedupeKey that stays the same each time the provider delivers that event:
| Provider | dedupeKey |
|---|---|
| Twilio | twilio:{MessageSid}:{MessageStatus} or twilio:{MessageSid}:received |
| Telnyx | telnyx:{data.id}, the webhook event ID |
| Plivo | plivo:{MessageUUID}:{Status} or plivo:{MessageUUID}:received |
| Vonage | vonage:{message_uuid}:{status} or vonage:{message_uuid}:received |
Store each key with a unique constraint and skip events you have seen:
import { parseSmsWebhook } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";
export async function POST(request: Request): Promise<Response> {
const event = await parseSmsWebhook({
provider: "telnyx",
request,
credentials: { publicKey: process.env.TELNYX_PUBLIC_KEY ?? "" },
});
const isNew = await db.webhookEvents.insertIfNew(event.dedupeKey);
if (!isNew) {
return new Response(null, { status: 200 }); // a repeat: acknowledge and stop
}
// Handle the event once.
return new Response(null, { status: 200 });
}
Answer repeats with a 2xx. An error response makes the provider retry again.
insertIfNew must be atomic, for example INSERT ... ON CONFLICT DO NOTHING returning whether a row was inserted. If you read first and insert second, two parallel deliveries can both pass. Keep keys at least as long as the provider retries webhooks, typically a day or more.
Deduplication also makes replayed Twilio and Plivo requests harmless. See Webhook security.
Handle out-of-order status
Never let a late event move a message backward. Rank the statuses and keep the highest:
import type { MessageStatusType } from "@opencoredev/sms-sdk/webhooks";
const rank: Record<MessageStatusType, number> = {
"message.queued": 0,
"message.sent": 1,
"message.delivered": 2,
"message.undelivered": 2,
"message.filtered": 2,
};
export function nextStatus(current: MessageStatusType | undefined, incoming: MessageStatusType): MessageStatusType {
if (current === undefined) return incoming;
return rank[incoming] >= rank[current] ? incoming : current;
}
console.log(nextStatus("message.delivered", "message.sent")); // "message.delivered"
The final states delivered, undelivered, and filtered share a rank, so the last one to arrive wins. Providers rarely send two different final states. When they do, occurredAt, if present, tells you which happened later.
Status before the send result
The provider can call back while your process is still handling the send() response, so a status webhook may arrive before you save the result. If you look messages up by providerId, either hold unknown IDs briefly and apply them once the send result is saved, or upsert the status row keyed by providerId.
Inbound repeats
A repeated inbound message has the same dedupeKey. If you reply, derive the reply’s idempotencyKey from it so a repeat cannot trigger a second reply. See Inbound SMS.