# Why did Stripe charge my customer twice?

> A customer was charged twice for one order on Stripe. How a second payment gets created, how to stop it with one PaymentIntent per order, and what to refund.

Canonical: https://app369.com/fix/stripe-customer-charged-twice/
Last updated: 2026-11-12

A customer writes in: they paid once and their card shows two charges. In your Stripe Dashboard there are two successful payments for the same amount, seconds apart. If both show as succeeded, two separate payments were created. Here’s what Stripe’s docs recommend so your checkout creates only one, or hand it over at a fixed price.

**Rather hand it over?** [App Fix Ticket](https://app369.com/fixed-price/flutter-firebase-bug-fix/): One reproducible bug in an existing mobile or web app, diagnosed and fixed.

[Pay $450 for a Diagnosis](https://buy.stripe.com/dRmdRa7K4bWz8CWdwagw003) [**Ask a question first**](https://app369.com/contact/?offer=offer1-diagnosis&amp;ask=1)

Full refund if Simon can’t take it on once he sees your project, or if you cancel before work starts. [Refund terms](https://app369.com/terms-of-service/#refunds).

- ![](https://app369.com/.netlify/images?w=28&amp;fm=webp&amp;url=%2Fimages%2Fszymon.jpg)Simon does the work himself
- [5.0 from 127 Fiverr reviews, checked Oct 30, 2026](https://app369.com/reviews/)

**Short answer**

If it is one order, your checkout created a second payment for it. This can happen when the customer presses Pay twice, reloads the payment page, or your server retries a request. Refund the duplicate in the Stripe Dashboard, then fix the cause: create one PaymentIntent per order and reuse it, send an idempotency key based on the order ID with every create request, and disable the Pay button while a payment is in progress.

Rather not fix it yourself? [**App Fix Ticket**](https://app369.com/fixed-price/flutter-firebase-bug-fix/) starts with a fixed-price diagnosis, then a fixed price for the fix before any fix work.

## **How the second charge happens, and the fix**

1. 01 ### **A double click or a reload starts a new payment**
  > “If the checkout process is interrupted and resumes later, attempt to reuse the same PaymentIntent instead of creating a new one.” [**Stripe, Payment Intents API (Best practices)**](https://docs.stripe.com/payments/payment-intents) If your code creates a new PaymentIntent each time the payment page loads or the Pay button is pressed, a second click or a refresh can start a second payment. Create the PaymentIntent once per order, save its ID with the cart or order, and fetch that same one again when the customer comes back. Disable the Pay button until the first attempt returns.
2. 02 ### **Your server retries without an idempotency key**
  > “The API supports idempotency for safely retrying requests without accidentally performing the same operation twice.” [**Stripe, Idempotent requests**](https://docs.stripe.com/api/idempotent_requests) A timeout or a dropped connection can make your server, or a job queue, send the same create request again. Without an idempotency key Stripe treats the retry as a new payment. With one, a retry that repeats the same key gets the first request’s result back.```
const paymentIntent = await stripe.paymentIntents.create(
  { amount: order.amountInCents, currency: 'usd', automatic_payment_methods: { enabled: true } },
  { idempotencyKey: \`order-${order.id}\` }
);
```
3. 03 ### **The key changes on every attempt**
  > “This key is typically based on the ID that you associate with the cart or customer session in your application.” [**Stripe, Payment Intents API (Best practices)**](https://docs.stripe.com/payments/payment-intents) An idempotency key only helps when the retry sends the same key. A random value made fresh on each click or each request gives every attempt its own key, so nothing is caught. Build the key from your own order or cart ID.

Checked against the vendor’s docs on November 12, 2026: [Stripe docs: Payment Intents API](https://docs.stripe.com/payments/payment-intents); [Stripe API reference: Idempotent requests](https://docs.stripe.com/api/idempotent_requests).

## **Rather hand it over?**

Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.

- [from $450**App Fix Ticket**One reproducible bug in an existing mobile or web app, diagnosed and fixed.**See scope and start**](https://app369.com/fixed-price/flutter-firebase-bug-fix/)
- [$950**Stripe Billing Fix and Setup**Subscriptions, webhooks, customer portal and failed payments set up or fixed, tested in Stripe test mode.**See scope and start**](https://app369.com/fixed-price/stripe-billing-setup/)
- [from $450**AI-Built App Rescue**A Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.****No issues worth fixing, no charge.**** If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.**See scope and start**](https://app369.com/fixed-price/ai-app-rescue/)

[**All 127 Fiverr reviews**](https://app369.com/reviews/)

Related: [**Stripe webhook 200 but database not updated**](https://app369.com/fix/stripe-webhook-200-but-database-not-updated/) · [**Paid on Stripe but account not upgraded**](https://app369.com/fix/stripe-paid-but-account-not-upgraded/) · [**Stripe dispute or chargeback**](https://app369.com/fix/stripe-dispute-chargeback/) · [**Refund a disputed Stripe payment**](https://app369.com/fix/stripe-refund-disputed-payment/)

Questions

## **More on this error**

<details>

<summary>**Should I refund the second charge? **+</summary>

Yes, if the customer received one order. Open the duplicate payment in the Stripe Dashboard and refund it in full, then tell the customer. A customer who sees two charges and hears nothing may dispute one with their bank instead.

</details>

<details>

<summary>**What if the amount changed between the two attempts? **+</summary>

Then do not create a new payment. Stripe compares a repeated idempotency key’s parameters with the first request and returns an error if they differ, so update the amount on the existing PaymentIntent instead.

</details>

<details>

<summary>**How long does an idempotency key protect a payment? **+</summary>

At least 24 hours. Stripe’s API reference says that for API v1 keys can be removed once they are at least 24 hours old, and a key reused after that starts a new request. The key guards against retries, not against a customer ordering again next week.

</details>

<details>

<summary>**Can you fix it for me? **+</summary>

If one bug is the whole problem, the App Fix Ticket covers it: a written diagnosis first, then the fix. If the app has more going on, the Launch Check is a written check of the whole app. Each issue found is rated critical, high, medium or low, with where it is and how it was confirmed. No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.

</details>

A different error?

## **Describe it and get a written quote**

Send the error and what you were doing. You get a written scope, a fixed price and a timeline; Simon replies by email, usually within one business day.

[**Describe your problem **→](https://app369.com/contact/) [**All fixes**](https://app369.com/fix/)