A webhook tells you what happened, not what is true now.
The same event may knock twice, and yesterday’s fact may arrive after today’s. Reliable systems preserve the evidence before deciding what it means now.
By Moosewave
Published · 10 min read

The short version
Store the fact before you act on it.
An email webhook is an HTTP notification about an event such as delivery, delay, complaint, unsubscribe, open, or click. It is not a live copy of current message or recipient state.
- 01Verify the sender against the original request bytes before parsing the payload or changing anything.
- 02Record a stable source and event ID durably, then return success without waiting for every downstream workflow.
- 03Expect valid retries and surprising arrival order; make both the handler and each business effect idempotent.
- 04Build transport, safety, and engagement views from the event log, then reconcile important records with the source.
The outward half of this contract is explained in A retry is not permission to send twice.
One conversation contains four different things
Your application requests an order receipt. The email API accepts it. Later, an email service observes delivery and calls your webhook endpoint. If the response disappears, it calls again. The two HTTP requests look like two things happening. They may be two attempts to report one thing.
This is the mirror image of uncertainty on the send path. A stable idempotency key stops repeated API attempts from becoming repeated email. A stable event identity stops repeated webhook attempts from becoming repeated workflow changes. Both are needed because a network can lose an acknowledgement without losing the work it acknowledges.
| Object | Meaning | Example |
|---|---|---|
| Occurrence | A fact observed in a system | A receiving server accepted the message |
| Event | A record describing that fact | delivery.completed / evt_71 |
| Delivery attempt | One transport attempt carrying it | POST number 1, 2, or 3 |
| Projection | The current view derived from facts | Recipient transport: delivered |
CloudEvents makes the distinction explicit: an event is a record of an occurrence, and one occurrence can yield more than one event. It defines source plus id as the event identity. Neither an HTTP request ID nor a hash of the whole payload is a universal substitute for the provider’s documented identity.
The webhook is a delivery mechanism for evidence. It is not the database where truth lives.
Make the receiver boring and fast
A webhook endpoint should do a small amount of important work. It should prove who sent the request, preserve what was sent, and make later processing possible. Rendering a report, updating six systems, or sending another message can happen after the provider connection has closed.
- 01
Preserve the original bytes
Signature schemes commonly cover the request body exactly as sent. Parsing and re-serialising it first can change the bytes required for verification.
- 02
Verify source and recency
Use the provider's signature header, endpoint secret or public key, and replay-window rules. Reject invalid requests before any side effect.
- 03
Insert one immutable event
Store workspace, source, event ID, type, schema version, occurrence time, message and recipient identity under a unique constraint.
- 04
Enqueue, then acknowledge
Commit a processing intent with the event, or make the durable queue the capture boundary, then return 2xx. A valid duplicate can receive the same success.
raw = request.body_bytes
verify(raw, signature, timestamp)
event = decode(raw)
transaction:
created = events.insert_once(
key = [workspace, event.source, event.id],
envelope = event
)
if created: outbox.insert_once(event.id)
return 204A relay publishes committed outbox rows to the worker queue. This closes the crash gap between storing the event and scheduling its processing; a durable queue can serve as the same boundary when its write is the first committed record.
Stripe documents automatic retries, duplicate events, unordered delivery, signature verification, asynchronous queues, and prompt successful responses. GitHub similarly recommends a quick 2xx and a processing queue. Exact timeout and signature formats differ, but the boundary remains: authenticate and capture before acknowledging; do expensive work later.
Keep endpoint secrets scoped and rotatable, map each event to the correct workspace, and retain an audit trail. Those controls belong beside the isolation boundaries on the Moosewave security page.
Deduplicate twice
The first deduplication answers a transport question: have we recorded this event before? A unique constraint on source and event ID makes that decision atomic. Two workers can race and still leave one record.
The second answers a product question: has this effect already happened? Recording evt_71 once does not help if a worker suppresses a recipient, publishes a notification, and crashes before marking the job complete. A retry can run the business action again.
- Event key
- workspace + provider source + provider event ID
- Projection key
- message + recipient + projection version
- Effect key
- business action + stable source fact
- Replay result
- the existing outcome, not a second effect
Where possible, commit the state change and effect record in one transaction. When the effect lives elsewhere, use a durable inbox or outbox and give the next operation its own idempotency key. AWS advises idempotent event consumers because duplicate processing can occur in managed systems.
Do not discard every similar payload as a duplicate. Stripe notes that separate event objects can sometimes describe the same object change. Preserve both records, then make the domain effect safe using documented object, event-type, recipient, and revision fields.
Arrival order is not event order
Webhooks travel on different schedules. An endpoint outage can leave one event in retry while a newer event succeeds on its first attempt. The HTTP arrival time describes your connection, not the chronology of the customer’s experience.
10:02
Occurred
Message accepted
10:05
Occurred
Recipient complained
10:08
Arrived first
Acceptance retry
10:09
Arrived second
Complaint event
10:11
Arrived late
Older delivery event
Now
Current decision
Remain suppressed
Shopify states that it does not guarantee ordering within or across webhook topics and recommends event timestamps. Timestamps help, but they are not the whole policy. Prefer a documented provider sequence when one exists. Otherwise use occurrence time, stable subject identity, and explicit domain rules.
A later arrival should add evidence, not overwrite a row blindly. If facts conflict, retain both, flag the projection, and reconcile with the source. This is how connected integrations exchange events without turning delivery order into customer state.
Email has several states, not one status
The label “sent” compresses too much. A request can be accepted while transport is delayed. A receiving server can accept a message without placing it in the inbox. A person can complain after delivery. An open or click is an observation, not permission to erase a safety decision.
| Lane | Typical evidence | Safe rule |
|---|---|---|
| Request | accepted, attempting, rejected, unknown | Do not infer mailbox delivery |
| Recipient transport | delayed, delivered, permanently failed | Project per recipient |
| Recipient safety | complaint, unsubscribe, durable suppression | Late transport cannot restore eligibility |
| Engagement evidence | open and click observations | Append evidence; do not upgrade permission |
Amazon SES documents these as different event families and identifies which recipients each event applies to. It defines delivery as handoff to the recipient’s mail server. That remains different from inbox placement and human action, as our guide to email delivery evidence explains.
Safety state deserves its own durable lane. A late open or delivery must not reverse an unsubscribe or complaint. The same principle underlies reliable unsubscribe handling. An event can trigger an action, but current eligibility decides whether an automation should continue.
Use webhooks for speed and reconciliation for repair
A webhook can still be missed. Your endpoint may be down until a retry window ends. A schema change may move an event into quarantine. A worker may exhaust its attempts. Treat replay and reconciliation as ordinary design, not emergency procedures added after the first gap.
Shopify recommends reconciliation jobs because webhook delivery should not be the only path to consistency. For important transactional email, compare unresolved records with the provider or source API. Rebuild projections from the durable event log. Replay a failed event without inventing a new identity.
- Queue lag
- How long valid events wait before processing.
- Oldest unresolved
- Whether one stuck event hides behind a healthy average.
- Duplicate rate
- How often retry traffic reaches the receiver.
- Signature failures
- Invalid, stale, or misconfigured deliveries.
- Projection conflicts
- Facts the state rules cannot resolve automatically.
- Reconciliation drift
- Facts at the source but absent from the local log.
Good email analytics begin with this separation between raw observation and derived view. Reports can change when better evidence arrives without rewriting the event that arrived first.
The product consequence
Moosewave connects the original application event, the transactional message identity, each authenticated webhook event, and the current recipient projection. Duplicate deliveries resolve to the existing event record. Processing happens behind a durable boundary, so a quick response does not require hurried business logic.
Message history keeps request, transport, safety, and engagement evidence distinct. A stronger safety fact cannot be erased by an older delivery event arriving late. Ambiguous records remain visible for replay or reconciliation instead of becoming a reassuring but false status.
Explore the contract on the Moosewave transactional email API page. See how commerce, CRM, support, and custom webhooks enter through the integration layer, then follow audience, automation, message, and evidence through the connected ecosystem.
Open the interactive product walkthrough to see that history move across the workspace. A reliable webhook system is not one that never sees repetition or disorder. It is one that sees both without changing what is true.
Frequently asked questions
Direct answers about email webhooks, duplicate delivery, event order, signatures, acknowledgements, and recovery.
What is an email webhook?
An email webhook is an HTTP notification sent when an email service observes an event such as delivery, delay, bounce, complaint, unsubscribe, open, or click. It describes an occurrence; it is not automatically a complete or current view of the message or recipient.
Can the same email webhook arrive more than once?
Yes. A provider may retry because it did not receive a successful response, even when your endpoint already stored the event. Give each source event a stable identity, store it under a unique constraint, and acknowledge a valid duplicate without repeating its effects.
Are email webhook events delivered in order?
Do not assume so unless the provider explicitly guarantees ordering for that event stream. Network retries, separate processing paths, and endpoint downtime can change arrival order. Use provider sequence data or occurrence times when available, plus domain rules and reconciliation.
When should a webhook endpoint return a 2xx response?
Return success after the request has been authenticated and the event has been captured durably or placed on a durable queue. Do not hold the connection open for every downstream workflow. Invalid signatures should be rejected, while already-recorded valid events can be acknowledged successfully.
How should an email webhook signature be verified?
Follow the provider's exact verification contract. It commonly requires the original request bytes, a signature header, an endpoint secret or public key, and a timestamp or replay window. Verify before parsing or performing a side effect, use HTTPS, and rotate secrets deliberately.
What is the difference between API idempotency and webhook idempotency?
API idempotency keeps repeated send requests from creating repeated emails. Webhook idempotency keeps repeated event deliveries from creating repeated state changes, notifications, or workflow actions. They protect opposite directions of the same distributed conversation.
How do you recover a missed webhook event?
Keep a replayable event log, expose failed deliveries, and periodically reconcile important records against the provider or source API. Webhooks provide timely notification; reconciliation repairs gaps caused by downtime, exhausted retries, schema errors, or application failures.
Sources & method
Event and webhook references
Official specifications and provider documentation support the event-delivery and email-event claims. The state model and product recommendations are Moosewave’s synthesis.
- Stripe documentation, Receive webhook events. Retries, duplicates, ordering, signatures, asynchronous processing, and successful responses.
- Amazon SES API v2, EventDestination. Definitions for send, delivery, delay, bounce, complaint, subscription, open, click, reject, and rendering events.
- Amazon SES, Published event record fields. Message IDs, timestamps, recipients, and delivery meaning.
- CloudEvents 1.0.2 specification. Occurrences, events, transport messages, and source plus ID identity.
- Shopify, About webhooks. Duplicate identity, unordered delivery, timestamps, and reconciliation.
- Google Cloud Pub/Sub, Exactly-once delivery. Acknowledgement uncertainty and push-delivery boundaries.
- GitHub Docs, Webhook best practices. Prompt
2xxresponses and asynchronous queues. - AWS Lambda, Function best practices. Idempotent consumers for duplicate event processing.
Last reviewed 9 August 2026. Webhook contracts, retry windows, and signature procedures vary; verify the documentation for every provider you connect.
Continue reading
Make the command safe before reading its events
The previous field note explains the other half of the conversation: why every retry of one transactional send must preserve one message identity.