A valid signature still needs a trust decision
An Oprindo sample had a matching asset hash and a valid signature, alongside an untrusted credential result. The useful story is in those separate answers.
Durable receipt, a membership transition and a welcome pack are separate obligations. Give each the identity and recovery boundary it needs.
The webhook can have arrived successfully while the new member is still inactive and their welcome pack has gone nowhere. The provider and the application have finished different jobs. One has delivered a message; the other has accepted work it still owes the member. A dashboard that gives receipt, activation and fulfilment one success status makes the missing-pack enquiry harder, because the colleague answering it must first discover which success that reassuring word refers to.
Consider a member whose joining payment produces a notification. Receiving it, activating the membership and arranging a welcome pack are three different obligations. In the example, account A1’s event is evt_7, the membership is M9 and the pack intention is F9 for this joining period. Keeping those references separate lets support trace the hand-offs without assuming that receipt proves fulfilment. It also lets the application recognise two notifications about the same entitlement without granting the member a second pack.
Stripe's current webhook documentation, used here to establish the delivery contract, describes duplicate delivery, potentially distinct events representing the same object and event type, and asynchronous processing. It also requires the unmodified request body for signature verification and recommends a prompt successful response before complex downstream work.[1] These are delivery constraints, not a complete membership design. The application still has to decide what it is accepting responsibility for when it returns that response.
The membership example gives receipt, transition and fulfilment their own scope.
Follow the labelled arrows. On a narrow screen, scroll sideways if needed.
evt_8 can be distinct receipt evidence while mapping to the same F9 obligation under the illustrative policy.A proposed receiver first verifies the request using the provider's supported signature procedure, identifies the provider account in its own trusted configuration, and decides whether it supports the event. Only then does it insert a durable inbox record keyed by that account and event identifier. For a supported message, successful acknowledgement follows the inbox commit. Merely placing an object in process memory is not the same boundary: a restart could remove the only copy after the sender has reasonably stopped waiting for work to be accepted.
Duplicate insertion is an ordinary outcome. A unique inbox key allows the receiver to recognise that it already holds evt_7 and acknowledge the redelivery without making a second processing job. A crash before the commit leaves no accepted inbox row; a crash after the commit but before the HTTP response leaves one. The sender may redeliver in the second case, but the inbox identity is still available. The two failures require different local observations even though both can appear to the sender as a failed request.
The downloadable SQLite example begins after signature and event-semantic checks. A rollback leaves its inbox empty; a committed receipt followed by redelivery leaves one pending evt_7 row. That is the local boundary the acknowledgement must follow. The network can still lose the response, but the next delivery has a durable identity to meet. An HTTP handler must make its successful response depend on that committed record, rather than on a worker having briefly seen the event in memory.
Accepting responsibility also means keeping enough to finish the job later. The model stores identifiers because its membership mapping is fixed. A real receiver needs the supported payload or a reliable retrieval reference, including its version and appropriate access controls. A pending row is of little comfort if its reference expires before a worker uses it. In that case duplicate detection can remain perfectly healthy while the accepted business work has become impossible to complete. Retention is therefore part of what the acknowledgement promises.
The worker's local task is precise: activate M9, record F9 as pending, and mark evt_7 processed. Where these records share a transactional database, they can be committed together. If the worker fails after changing membership but before creating the fulfilment intention, a rollback should remove both changes. Otherwise the next worker may see an active member and incorrectly conclude there is no remaining work, leaving the missing welcome pack to be found by a customer enquiry.
The rollback check left M9 inactive, no fulfilment row and evt_7 pending. A successful transaction left M9 active, F9 pending and the event processed; running the worker again changed nothing.[2] A real multiworker service must arrange exclusive ownership or equivalent coordination before applying that transition. A stalled claim also needs recovery, because leaving the receipt safely pending is useful only while another permitted worker can continue it. Serial SQLite checks establish the stated local outcomes, while those competing-worker rules belong to the deployed database design.
The important boundary is wider than one successful INSERT. Three independent writes with three commits would expose intermediate combinations. Equally, making the HTTP handler wait while a warehouse API responds would place external work inside the acceptance path without making that external API part of the local transaction. Keeping a durable fulfilment intention separates what the application has decided from whether the supplier has acted. The operator can see an active membership with a pending pack as a real state, rather than an impossible accident.
The processed marker should mean that the event's local transition has been dealt with. Fulfilment may still be pending. Erasing the marker and replaying the whole payment reaction after a dispatch failure risks repeating local work that was already correct. Retaining F9 makes recovery more precise: the operator can investigate that obligation while the accepted event and completed membership transition remain recorded with their original meanings.
Then evt_8 arrives. It is a genuinely different event, but the example’s business rule maps it to the same M9 joining generation. There are now two processed inbox rows and still one F9. A separate uniqueness rule on membership and purpose, welcome:generation-1 here, makes that possible. The event names are invented rather than real Stripe event types; their supplied mapping lets us examine why accepting another receipt need not create another entitlement.
It would be tempting to make membership alone unique forever. That could suppress a legitimate future pack for another joining period. Conversely, using event identity as the fulfilment key would permit two different notifications about the same entitlement to create two packs. The right key describes the business obligation. Writing its scope in ordinary language before choosing columns is useful: one welcome pack for this membership's first joining generation, rather than one welcome pack for each message received.
This is also where a generic duplicate rule reaches its limit. Two object updates can be distinct and both legitimate; collapsing every event with the same object identifier can lose a real change. A cancellation or a later renewal cannot simply be treated as another activation. Their ordering and transition policies need separate design. The early receiver considered here does not solve that problem by remembering event identifiers. It gives a later state machine a reliable set of accepted inputs to work from.
Dispatching the physical pack is another step again. Where the supplier supports a suitable operation identity, F9 should stay with the dispatch attempt; an uncertain response then needs investigating under that supplier’s contract. The model stops before this point. It has shown us an obligation that survives the local work, not exactly-once delivery to a warehouse. Renaming the pending row sent because the worker committed would make the database tidier at the expense of the member still waiting for the pack.
Executed local SQLite model; signatures, HTTP transport and pack dispatch are outside its scope.
Scroll sideways to see all columns.
| Interruption / input | Inbox | M9 | Pack intention |
|---|---|---|---|
| Receipt rollback | No row | Inactive | None |
| Commit; lost reply; redelivery | evt_7 pending, once | Inactive | None |
| Worker rollback | evt_7 pending | Inactive | None |
| Worker commit; repeated run | evt_7 processed | Active | F9 pending, once |
Different evt_8, same generation | Two processed events | Active | F9 pending, once |
A separate inbox and worker are not mandatory for every notification. If the complete supported action is a short local transaction, an endpoint may be able to apply it and acknowledge after commit, within the sender's constraints. That can be simpler than operating another queue. The trade-off changes when processing calls slow services, needs independent recovery, or can be accepted while an external dependency is unavailable. The architecture should follow those obligations, rather than assume that more moving parts automatically make receipt reliable.
For the inbox version, the operational view should expose the oldest pending event, its last processing failure and the fulfilment identity it produced. A row that is safely stored but never revisited is not a successful integration. Retention must also be deliberate: deleting the receipt or business obligation can remove the information that makes a late delivery recognisable. There is no universal lifetime that can be inferred from this model; provider behaviour and the business recovery window both matter.
For the colleague answering that missing-pack enquiry, the investigation can now stay attached to three records: accepted evt_7, active M9 and F9 with whatever dispatch outcome has actually been confirmed. Each narrows what remains to do. The successful webhook receipt still matters, but it no longer has to stand in for the whole service. It says the application accepted the responsibility, while the membership and pack records show how far it has carried that responsibility through.
Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.
See the Payment Operations PilotPrimary sources behind this article. Living documentation may change.

Practical engineering. A human perspective.
More about Hamsa Software and the person behind it. Editorial notes.