# How do retries avoid charging a card twice?

Authors: AutoRouter
Author profile: https://autorouter.com/about
Published: 2026-11-05T21:10:00.000Z
Reading time: 5 min
Canonical: https://autorouter.com/blog/how-retries-avoid-double-charges
Publisher: AutoRouter, the Mac app for Codex and Claude Code (https://autorouter.com)

> A retry avoids a second charge when the server can tell it is the same request as the first. Stripe does this with idempotency keys: the client sends a unique key with each request, and Stripe replays the saved result to any retry that carries it. Stripe may prune a key once it is at least 24 hours old, so a billing system also records each charge before money moves.

## Key facts

- With Stripe's API v1, a retry that reuses an idempotency key gets the first request's saved status code and body back, `500` errors included ([Stripe](https://docs.stripe.com/idempotency), read November 4, 2026).
- Stripe may prune an idempotency key once it is at least 24 hours old, and reusing a pruned key creates a new request ([Stripe](https://docs.stripe.com/idempotency), read November 4, 2026).
- AutoRouter records each prepaid seat charge before any money moves, so a retried seat change reuses the charge it already made.

A retry avoids a second charge only when the server can recognize it as the first request again. Stripe's [idempotency keys](https://docs.stripe.com/idempotency) do that inside Stripe, and a billing system's own record of each charge does it everywhere else. [AutoRouter, the Mac app for Codex and Claude Code](https://autorouter.com), builds its billing on that pattern: a retried seat change reuses the charge it already made, and an account holds one Pro subscription even when two checkouts start at once. AutoRouter wrote this post, and Stripe's behavior in it comes from Stripe's docs as read on November 4, 2026.

## Why can a retry charge a card twice?

A retry can charge twice because a request can fail after the server has already done the work. When a connection drops mid-request, the client can't tell whether the charge went through, and a blind retry repeats it. Stripe's engineering blog describes these ambiguous failures and pairs idempotency keys with exponential backoff and jitter, so retries from many clients don't arrive at a struggling server all at once ([Stripe, February 22, 2017](https://stripe.com/blog/idempotency)).

## What does Stripe save under an idempotency key?

For API v1, Stripe saves the status code and body of the first request made with a key once the endpoint starts running, and every retry with that key gets the saved response back, `500` errors included ([Stripe: Idempotent requests](https://docs.stripe.com/idempotency)). Stripe also compares each retry's parameters with the first request's and returns an error when they differ. A request that fails validation, or that collides with another request still running, isn't saved, so the client can retry it. With API v2, retries don't repeat work that succeeded, and Stripe tries to finish work that failed or partly failed.

| What happened to the first request (API v1)      | What a retry with the same key gets      |
| ------------------------------------------------ | ---------------------------------------- |
| It succeeded                                     | The saved success response               |
| It returned a `500`                              | The same saved `500`                     |
| The retry's parameters differ                    | An error                                 |
| It failed validation or hit a concurrent request | A fresh attempt, since nothing was saved |
| Its key was pruned after 24 hours or more        | A new request                            |

## How long does an idempotency key protect a retry?

At Stripe, an idempotency key protects retries for at least 24 hours: keys can be pruned once they are at least 24 hours old, and reusing a pruned key creates a new request ([Stripe](https://docs.stripe.com/idempotency)). Resend's email API works the same way, checking for an email sent with the same key in the last 24 hours ([Resend](https://resend.com/docs/dashboard/emails/idempotency-keys)). A job that keeps retrying for days outlives its key, so it needs its own record of what already happened.

## What should a billing system record before it charges?

A billing system should record each charge before it calls the payment provider, so every retry finds the same charge and resumes it. Four steps cover it:

1. Save the charge, with its amount and account, before calling the provider and outside any database transaction that can roll back.
2. Derive one idempotency key for each step from that record's ID, so every retry can rebuild the same keys.
3. On every retry, resume the saved charge with the same keys.
4. Mark the charge done only after the provider's answer is saved.

Step 2 has one catch. Because Stripe replays a saved `500` to every retry with the same key, a step that failed with a server error needs a new key before it can succeed, and the system should first check what already went through so the new attempt doesn't repeat it.

## What does AutoRouter do so you're charged once?

AutoRouter's billing is built so each payment happens once and the credit you get matches what you paid:

- **One Pro subscription per account.** Two Pro checkouts started at once, in two tabs or on two devices, still end in one subscription.
- **Seat changes charge once.** AutoRouter records each prepaid seat charge before any money moves, and a retried seat change resumes that charge with the same Stripe keys, so it reuses the charge it already made.
- **Credit matches payments.** Enterprise seat credit covers the period the paid invoice paid for, and a refund that arrives before its credit grant still takes that credit back.
- **Removed seats keep no credit.** Removing a seat takes back credit from a join that was still in progress, so a team doesn't pay for credit no seat stands behind.

The same rule covers email. A waitlist follow-up is claimed before it sends, and a send that may have gone out is retried with the same key only within 23 hours, inside Resend's 24-hour window, so each follow-up sends once. For the rest of a team rollout, from sign-in to budgets, see [How to roll out AI coding tools to an engineering team](/blog/ai-coding-tools-for-teams).

## Questions

### Does a retry with the same idempotency key ever charge twice?

Not while Stripe still holds the key, because a retry with that key gets the first request's saved result. Once a key is pruned, which Stripe may do after 24 hours, the same key starts a new request, and a retry with a different key is always a new request ([Stripe](https://docs.stripe.com/idempotency)).

### What makes a good idempotency key?

Stripe suggests V4 UUIDs or another random string with enough entropy to avoid collisions, up to 255 characters, and advises against sensitive data such as email addresses ([Stripe](https://docs.stripe.com/idempotency)). Deriving the key from your own charge record's ID lets every retry rebuild the same key.

### Why does a retried Stripe request return the same error?

With Stripe's API v1, the first response under a key is saved even when it is a `500`, so retries with that key get the same error back. Requests that failed validation or collided with a concurrent request weren't saved and can be retried as they are ([Stripe](https://docs.stripe.com/idempotency)).

### What does AutoRouter bill for?

AutoRouter Free is $0 a month and routes across your ChatGPT and Claude plans. Pro is $50 a month with $20 of included usage each month, and paid models are billed at the provider's price plus 10%, from included usage first and then from credits you top up. Credits you buy never expire. [How to cut Claude Code and Codex costs](/blog/cut-claude-code-and-codex-costs) covers the other ways to spend less.

## Sources

- [Stripe: Idempotent requests](https://docs.stripe.com/idempotency), read November 4, 2026
- [Brandur Leach, Stripe: Designing robust and predictable APIs with idempotency](https://stripe.com/blog/idempotency), February 22, 2017
- [Resend: Idempotency keys](https://resend.com/docs/dashboard/emails/idempotency-keys), read November 4, 2026
- [AutoRouter pricing](https://autorouter.com/#plans)

## Read next

- [Which effort level is a fair baseline for router savings?](https://autorouter.com/blog/router-savings-effort-baseline.md): A fair baseline for a router's savings is the cheapest effort level that reaches the model's top score on the same benchmark. On DeepSWE, GPT-6 Astra reaches its top score of 74% at xhigh for $4.43 per task, while max costs a reported $7.50 for a score within the same error range. A router costing $3.00 per task looks 60% cheaper than Astra at max and 32% cheaper than Astra at xhigh.
- [How do you test a model router change before it ships?](https://autorouter.com/blog/how-to-test-a-routing-change.md): Test a routing change by running real, reviewed requests through the same code path production uses, rules included, and grading each pick against the models a reviewer accepted. A test that skips the rules can pass while the shipped policy breaks one. AutoRouter, the Mac app for Codex and Claude Code, checks its routing changes against reviewed cases that include picks people rated down.
- [What text does a model router read in a Claude Code turn?](https://autorouter.com/blog/what-text-a-router-reads-in-claude-code.md): A model router should judge a Claude Code turn by the request the person typed in it. Claude Code also adds its own text to the conversation, and CLAUDE.md files arrive as a user message after the system prompt, so a one-line request can sit beside pages of project notes. Run /context all in a session to see which memory files loaded.

Tags: [Costs](https://autorouter.com/blog/tags/costs.md), [Teams](https://autorouter.com/blog/tags/teams.md), [AutoRouter](https://autorouter.com/blog/tags/autorouter.md)

Drafted with AI from the sources linked above, then checked and edited by the AutoRouter team.
