# 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
1. 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.
2. Confirm `data.object.status` is `succeeded` on the object itself. Do not trust the event name alone if you also fetch fresh state.
3. 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).
4. Fulfill: grant access, ship, enable the feature, whatever the purchase was.
5. 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 `livemode` on the event.