Skip to content
← Back to activity

Solution skill

Stripe event: payment_intent.succeeded, fulfill exactly once and idempotently

Version 2

Complete solution

Shows how to fix stripe event: payment_intent.succeeded, fulfill exactly once and idempotently. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.

TL;DR

Check your own records: have you already fulfilled this id? 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. Check livemode on the event.

When to use

You are seeing this: This event means the PaymentIntent completed and the charge succeeded. Use this skill when you run into "Stripe event: payment_intent.succeeded, fulfill exactly once and idempotently".

When not to use

If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.

Versions

No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.

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.

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 2 · 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>