The API response is not the delivery status

When your send request succeeds, you usually know one thing: the messaging platform accepted your request. Delivery is asynchronous. The carrier may accept, delay, filter, expire, reject, or report a status later. Your application needs SMS delivery tracking webhooks because the truth changes after the initial API response.

Twilio describes outbound status callbacks as a way to track status changes through the message lifecycle. Vonage notes that a successful SMS API response means a message was queued, not necessarily delivered, and that delivery receipts vary in reliability by market and receipt type.

If your database only has sent = true, your support team has no delivery tracking. It has a guess.

That same event stream can power dashboards for OTP success rates, delayed messages, fallback usage, and support timelines, as long as each callback is stored idempotently and tied back to the original send request.

Design the send side first

Delivery tracking is easier when the original transactional SMS request already carries a use case, template key, message metadata, idempotency key, and stable message ID.

Read the transactional SMS API guide

SMS delivery tracking webhooks vs inbound SMS webhooks

SMS webhook searches often mix three related ideas: delivery tracking webhooks, inbound SMS receive webhooks, and OTP verification events. They should not all update the same field. A delivery callback explains what happened after your app sent a message. An inbound webhook captures a user reply. A verification event records whether the user completed, retried, expired, or abandoned an OTP flow.

Webhook typeWhat it tells youWhere to store it
Delivery status callbackThe provider or carrier changed the outbound message status, such as sent, delivered, failed, expired, or unknown.Message delivery timeline.
Inbound receive webhookA recipient replied to a number, short code, or sender that can receive messages.Conversation, reply, opt-out, or support intake record.
OTP verification eventA user requested, retried, checked, completed, failed, or timed out a verification attempt.Verification attempt timeline.
Retry eventYour platform or provider retried delivery, resent an OTP, or reprocessed a webhook callback.Retry log linked to the original message or attempt.

Keeping those timelines separate makes dashboards easier to trust. Support can see whether an OTP failed because the SMS was not delivered, because the user entered the wrong code, because a fallback path was used, or because an inbound reply belongs to a different workflow entirely.

What makes SMS webhooks and status callbacks reliable?

If you are comparing SMS APIs for reliable webhooks and status callbacks, do not judge reliability by the send response alone. Test how the provider reports accepted, sent, delivered, failed, expired, and unknown states, how quickly callbacks usually arrive, and whether retries or duplicate callbacks can be tied back to the original message ID.

Delivery receipts are provider and carrier signals, not proof that a human read the message. The practical goal is visibility: your product and support teams should know whether a message was queued, handed off, reported delivered, failed, delayed, or still missing a final state.

QuestionWhy it matters for deliveryWhat to store
Which delivery states are exposed?A useful callback feed should distinguish accepted, sent, delivered, failed, expired, rejected, and unknown states.Normalized status, raw provider status, timestamp, and final-state flag.
How are retries reported?Retries can create duplicate or delayed events, especially when providers retry webhook delivery after an endpoint error.Idempotency key, provider message ID, retry count, and callback attempt timestamp.
How fast do callbacks arrive?Status latency affects OTP fallbacks, support timelines, and dashboards for time-sensitive account alerts.Send time, callback received time, provider event time, and callback latency.
Can dashboards filter by country?Country-level views help you spot market-specific delivery changes without mixing them into global traffic.Destination country, sender identity, template, route, provider, and final outcome.
What happens when no final receipt arrives?Some messages may stay unknown, so the product needs a safe fallback and support explanation.Unknown state, timeout window, fallback offered, and user outcome.

A strong delivery dashboard should separate message delivery from user completion and still let teams filter by country. For OTPs, track whether the user verified after the message was sent. For account alerts, track whether the alert record has enough evidence for support to explain what happened without overclaiming that the recipient saw the SMS.

Normalize provider events into your own model

Every provider has its own payload names, status vocabulary, retry behavior, and error codes. Store the raw payload, but do not make the rest of your product depend on raw provider fields. Normalize events into a compact lifecycle that your app understands.

Normalized statusTypical provider meaningFinal?
acceptedAPI request accepted or message queued.No
sentMessage handed to the downstream network or messaging channel.No
deliveredProvider or carrier received a successful delivery receipt.Usually
undeliveredDelivery receipt says the handset or destination was not reached.Yes
failedProvider could not send, route, or process the message.Yes
expiredCarrier retry window ended before delivery.Yes
rejectedCarrier, provider, or policy rejected the message.Yes
unknownNo useful final state is available.Maybe

Do not assume events arrive exactly once or in perfect order. Webhook systems retry, networks fail, and providers can add fields over time. Design your event processor to be idempotent and tolerant of extra payload data.

The records worth storing

You need two levels of storage: the current message summary for quick reads, and an append-only event timeline for audit and debugging. The summary powers dashboards and product state. The timeline explains how the message got there.

FieldWhereWhy
message_idmessagesStable internal ID used by your app.
provider_message_idmessagesLets you reconcile with provider logs and support.
recipient_hashmessagesUseful for debugging without exposing phone numbers broadly.
destination_countrymessagesDelivery behavior and rules vary heavily by country.
sender_identitymessagesSeparates 10DLC, toll-free, short code, sender ID, or route behavior.
template_keymessagesHelps detect template-specific filtering or copy problems.
current_statusmessagesFast product reads and support filtering.
event_idmessage_eventsWebhook deduplication when the provider supplies a unique ID.
statusmessage_eventsThe lifecycle state from each callback.
error_codemessage_eventsDebugging, alerting, and provider support escalation.
raw_payloadmessage_eventsFuture-proof audit trail when mappings change.
received_atmessage_eventsYour system time, separate from provider event time.
create table sms_messages (
  id text primary key,
  provider_message_id text,
  recipient_hash text not null,
  destination_country text,
  sender_identity text,
  template_key text,
  current_status text not null,
  created_at timestamptz not null,
  updated_at timestamptz not null
);

create table sms_message_events (
  id text primary key,
  message_id text not null references sms_messages(id),
  provider_event_id text,
  status text not null,
  error_code text,
  provider_occurred_at timestamptz,
  raw_payload jsonb not null,
  received_at timestamptz not null
);

Build the webhook handler like an ingestion pipeline

Twilio's webhook security docs recommend HTTPS and signature validation, and warn that webhook parameters can evolve. That is the right shape for any provider integration: validate authenticity, preserve the raw request, map only the fields you understand, and do not break when new fields appear.

  1. Receive the raw request body before any middleware mutates it.
  2. Verify the provider signature using the exact URL, headers, and raw body or form parameters required by that provider.
  3. Reject invalid signatures before writing state.
  4. Map the provider message ID and status into your normalized lifecycle.
  5. Deduplicate by provider event ID when available, otherwise by provider message ID, status, and provider timestamp.
  6. Write the event and update the current summary in one transaction.
  7. Return 2xx only after the event is safely stored.
  8. Send unknown statuses to a dead-letter or review queue instead of dropping them.
async function handleSmsWebhook(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get("x-provider-signature");

  if (!verifyWebhookSignature({ rawBody, signature, url: request.url })) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = parseProviderPayload(rawBody);
  const normalized = normalizeSmsEvent(event);

  await db.transaction(async (tx) => {
    await tx.smsMessageEvents.upsert({
      providerEventId: normalized.providerEventId,
      messageId: normalized.messageId,
      status: normalized.status,
      errorCode: normalized.errorCode,
      rawPayload: event
    });

    await tx.smsMessages.updateCurrentStatus(normalized.messageId, normalized.status);
  });

  return new Response("ok", { status: 200 });
}

Handle duplicates and out-of-order events

Webhook delivery is usually at-least-once. That means duplicate callbacks are normal. If your handler increments counters, sends user notifications, or triggers fallbacks on every callback without deduplication, one provider retry can become a product bug.

Out-of-order events are just as important. You might receive accepted after delivered, or a delayed intermediate state after a final state. Keep an explicit status precedence model so old intermediate events do not downgrade a message that already reached a final state.

ProblemBad behaviorBetter behavior
Duplicate delivered eventSend the user two success notifications.Upsert by event ID and make side effects idempotent.
Late sent event after failedChange the message back to in progress.Store the event but keep the final summary state.
Unknown provider statusThrow 500 forever or drop it silently.Store raw payload, mark unknown, and alert engineering.
Provider outageLose callbacks during downtime.Return non-2xx only when storage failed and rely on provider retries plus reconciliation jobs.

Give support a real delivery timeline

The best delivery tracking work shows up in support. A support agent should be able to answer: when did the user request the SMS, what number was used, which sender identity sent it, what did the provider say, what did the carrier say, did we retry, and what should the user do next?

  • Show timestamps in the user's local timezone and UTC.
  • Mask phone numbers by default, with audited reveal access for trusted support roles.
  • Translate provider errors into plain-language support notes.
  • Link every message to the product action that triggered it.
  • Expose country, sender identity, provider, and template so patterns are visible.
  • Make it easy to copy a provider message ID for escalation.

This is also where product analytics becomes useful. If OTP completion drops in one country, your webhook data can show whether users are failing to request codes, carriers are rejecting messages, or receipts are delayed.

Use tracking to improve OTP completion

For verification flows, connect webhook events to resend timing, abuse checks, fallback choices, and the final verification outcome so OTP reliability is measured by completed logins, not accepted sends.

Read the OTP delivery guide

SMS webhook FAQ

Which SMS API has reliable webhooks and status callbacks?

Choose an SMS API by testing its callback lifecycle, not by the send response alone. Look for clear accepted, sent, delivered, failed, expired, and unknown states; idempotent retry handling; provider message IDs; country filters; and raw payload storage for support review.

Should webhook handlers return 200 immediately?

Return 2xx only after you have validated and durably stored the event. If storage fails, a non-2xx response lets the sender retry instead of losing the event.

Do I need to store raw webhook payloads?

Yes. Store them with access controls. Raw payloads help when provider fields change, mappings are wrong, support escalates, or you need to reconcile with provider logs.

Can I trust delivered as a final truth?

Use delivered as the best available delivery signal, but avoid wording that guarantees a human saw the message. Providers document cases where delivery receipt certainty varies.

Should I poll instead of using webhooks?

Polling can be useful for reconciliation, but webhooks should be the primary path for timely delivery updates. A nightly reconciliation job can catch missed or inconsistent events.