At-least-once delivery is the contract

Every major platform documents its webhooks as at-least-once. A network blip on the acknowledgement, a retry after a slow response, a manual replay during debugging — all of them deliver the same event twice. If your handler is not idempotent, each of those becomes a duplicate invoice.

Choose a stable idempotency key

The key has to come from the source event, not be generated on receipt. A Shopify order id, a payment reference, a composite of order-id plus event-type. Store it, and make the write conditional on it not already being present.

Dedupe windows and replay

Keep processed keys long enough to cover the platform’s maximum retry window plus your own replay habits — 30 days is a common choice. With that in place, replaying a day of events to recover from an outage is safe rather than terrifying.