Normalize provider events into one shape
Provider webhook payloads differ. Your application should store the raw payload for evidence, then map it into one normalized event shape that product, analytics, support, and retry workers can understand.
A good normalized shape is intentionally boring. It should identify the internal message, the provider message, the event type, when the event happened, when your system received it, and whether there is an error category to act on. That gives the product a stable contract even when provider-specific fields, nested objects, or naming conventions differ. The raw payload still matters, but it should not be the only thing your support tools can read.
{
"messageId": "msg_123",
"providerMessageId": "provider_456",
"status": "delivered",
"occurredAt": "2026-05-26T09:30:00.000Z",
"receivedAt": "2026-05-26T09:30:02.000Z",
"countryIso2": "US",
"senderIdentity": "+15551234567",
"errorCode": null,
"errorCategory": null,
"rawPayloadId": "payload_789"
}Keep the original body, headers, signature result, provider account, and receive timestamp alongside the normalized event. That audit trail helps when a webhook is retried, transformed incorrectly, or disputed during a support escalation. It also lets engineering replay events into a test handler without guessing which provider field created the final message state.
Delivery event examples
| Event | Meaning | Safe update |
|---|---|---|
| sent | The message left the messaging platform toward downstream delivery. | Record sentAt, but keep the message pending for critical flows. |
| delivered | A provider or carrier signal says delivery succeeded. | Mark delivered if the event is newer than the current state. |
| failed | The delivery path failed before success. | Mark terminal failure and attach error category. |
| expired | The delivery window ended. | Stop blind retries for time-sensitive OTPs and show fallback. |
| duplicate | The provider retried or resent an already processed event. | Store receipt evidence without changing state twice. |
The difficult cases are the ones between the rows. A duplicate delivered event should not create a second notification. A failed event that arrives after a delivered event needs a precedence rule. An unknown state should not be treated as success just because the API request was accepted. Build the handler so every event is recorded, but only valid transitions update the user-facing state.
Handler rules that prevent bad timelines
Treat webhook handling as persistence first and interpretation second. The endpoint should verify the request, store enough evidence, enqueue any slower processing, and answer quickly. Business logic such as user notifications, retries, fraud review, and support timeline updates can run after the event is safely captured. That design prevents a slow downstream job from causing duplicate provider retries.
- Verify the webhook signature before trusting the payload.
- Store raw body, headers, provider, and received timestamp.
- Deduplicate by provider event ID when available.
- Ignore stale events when a newer terminal state already exists.
- Return a quick 2xx response after the event is safely stored or queued.
Add alerting around the handler itself. A spike in signature failures, duplicate events, unknown statuses, or processing lag can be the first sign that delivery tracking is drifting. Those alerts help the team fix the integration before support starts answering message-by-message tickets with incomplete state.
Make delivery webhooks support-ready
Use Notilify to keep raw events and normalized delivery states together.
Read delivery tracking guide