Making Stripe webhooks idempotent in ASP.NET Core
The author explains an inbox pattern that stops a Stripe webhook from applying a payment twice when deliveries repeat or overlap. The sample verifies the raw body, claims events with a unique-key insert, and commits the claim together with the local payment change. Its 38-test suite exercises duplicates, rollback and retries, making it a concrete reference for payment flows in ASP.NET Core.
Key points
- The Stripe event ID is the deduplication key, so two deliveries compete for one row.
- Signature verification runs on the original request body, not a re-serialized copy.
- A unique-index insert decides the winner; no preliminary existence query is used.
- Events are stored as Pending before the 200 response, and a sweeper requeues due rows after restarts.
- The claim update and the local payment change commit in one transaction.
- External side effects such as email or API calls need their own outbox and idempotency.
Stripe can redeliver a webhook, and two deliveries can overlap while the first is still running. The sample uses the Stripe event ID as the deduplication key, verifies the signature on the raw body, and inserts the event into an inbox table with a unique index. The duplicate touches no rows and is simply acknowledged.
The processor then claims the event with a conditional update and commits that claim alongside the payment change in one transaction, so a failed handler rolls back both. Events that exhaust their retries are marked Failed for follow-up. The guarantee covers only local database effects; emails and external API calls still need an outbox and downstream idempotency.
Real-world use
- An ASP.NET Core billing API handling subscriptions and one-time payments that must accept Stripe webhooks safely.
- A background worker that updates local payment state and may receive the same event several times.
- A service running on several instances that must avoid double-applying payments during retries.
Why it matters
Repeated and concurrent webhook deliveries are normal, and an in-memory set of IDs fails across restarts and multiple instances. A database constraint plus a transactional claim keeps payment records consistent under those conditions.
Related stories
- ASP.NET Core Authentication Endpoints: Why I Built AuthEndpointsDEV Community: #aspnetcoreOct 22
- A secure enterprise chat with React, .NET 10 and Microsoft FoundryDEV Community: #dotnet1 d ago
- ASP.NET Core Authentication & Authorization Explained: What to Use, When, and How to Ship It SafelyDEV Community: #aspnetcore1 d ago