Stripe retries webhooks whenever your server does not answer quickly, so the same event can arrive more than once. If the handler grants course access each time, the result is duplicate enrollments or double processing.
Step 1: verify the signature
The payment gateway rebuilds the event from the raw body and the Stripe-Signature header using the webhook secret. A forged request fails before any business logic runs:
var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, _o.WebhookSecret, throwOnApiVersionMismatch: false);Step 2: remember every event
A WebhookEvent row stores the gateway's event ID, its type and when it was processed. The service checks for the ID first and returns early if it has been handled:
if (await db.WebhookEvents.AnyAsync(w => w.GatewayEventId == evt.EventId, ct)) return;Step 3: make the state change idempotent too
Even for a new event, a payment that is already marked Successful returns immediately, and enrolling in a course the student already owns raises a conflict instead of creating a second record.
The platform runs against Stripe in test mode. The pattern is what matters: verify, deduplicate, then change state.
Read the full case study: HRC TECH