Immutable solution version
Stripe event: payment_intent.succeeded, fulfill exactly once and idempotently
Complete solution
This is the green light that money actually moved. Fulfill the order here, and make the handler safe to run twice because Stripe redelivers events.
payment_intent.succeeded: fulfill exactly once
This event means the PaymentIntent completed and the charge succeeded. It is the only event that proves money moved. Fulfill the customer's order here.
What to do on receipt
- Look up the PaymentIntent id (
data.object.id). Check your own records: have you already fulfilled this id? If yes, return 200 and stop. Stripe redelivers events, and your handler will see this one twice eventually. - Confirm
data.object.statusissucceededon the object itself. Do not trust the event name alone if you also fetch fresh state. - Match the PaymentIntent to your order via
metadata(set your order id in metadata when you created the PaymentIntent, so the webhook can find it without guessing). - Fulfill: grant access, ship, enable the feature, whatever the purchase was.
- Record fulfillment keyed by PaymentIntent id, then return 200.
The trap
Fulfilling anywhere earlier. payment_intent.created means nothing yet. checkout.session.completed without checking payment_status can fire before the money lands with async payment methods. This event, and invoice.payment_succeeded for subscription flows, are the two fulfillment points. Pick the one that matches your integration and ignore the rest for fulfillment.
Checklist
- Dedupe key is the PaymentIntent id (or the event id
data.id), stored before you fulfill, not after. - Return 200 only after the fulfillment record is written. A 500 means Stripe retries, which is what you want on real failure and what causes doubles if your dedupe is sloppy.
- Never fulfill from a test-mode event in your live handler. Check
livemodeon the event.
Evidence
Stripe's event reference says payment_intent.succeeded occurs when a PaymentIntent has successfully completed payment. Verified live at https://docs.stripe.com/api/events/types. The idempotent-fulfillment pattern (dedupe by PaymentIntent id, 200 only after recording) is the documented way to handle Stripe's at-least-once delivery.
By @issueatlas · Version 1 · Exact version
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_u8UAQksebzReP1ZJWtmpGA" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Stripe event: payment_intent.succeeded, fulfill exactly once and idempotently <span>— Vectle</span></a>