← All posts
·3 min read·Reuben Santoso

Network Timeouts Cause Double Charges: The Idempotency Key Bug

Your Stripe/Resend call retries without idempotency keys. Customer charged twice. Here's why api-doctor catches it.

Quick Answer

An idempotency key is a unique identifier you attach to an API request so that retrying it never runs the operation twice. Networks flake. Requests time out, your app retries, and without an idempotency key that retry looks like a brand new request to the server. So the charge runs twice. The email sends twice. The invoice gets created twice. Idempotency keys are how providers promise "exactly once," and your AI agent almost never adds them. api-doctor catches it before you ship.

The problem: retries are silent duplicates

Let me put the two worlds side by side.

Without an idempotency key: your charge request times out, your app retries, and your customer gets billed twice.

With an idempotency key: your charge request times out, your app retries with the same key, the server hands back the cached response, and your customer gets billed once.

An idempotency key is just a unique identifier, usually a UUID, that you send along with every mutation request. If the request fails and you retry with the same key, the server replays the cached response instead of running the whole thing again.

Here's where the AI trips. It writes the happy path. Retry logic tends to live somewhere else, and the model doesn't connect the two, so it never realizes idempotency keys are the thing that makes retries safe.

A real example: the Stripe retry loop

Your agent writes a charge function with retry logic but no idempotency keys. First call times out. Retry after one second, times out again. Retry after two seconds, and this time it succeeds. Trouble is, Stripe already processed that first charge. Your customer just paid twice, and they won't find out until they scroll through their bank statement.

Stripe's own docs are clear about this: include an idempotency_key on every request, and Stripe will return the cached response if it sees the same key twice within 24 hours. The AI just never read that page.

How api-doctor catches this

✗ Stripe: 2 issues

  stripe-missing-idempotency-key
    src/orders.ts:12
    Missing idempotency_key. Retried requests will charge customers twice.
    Severity: error
    Reliability

How to fix it: add idempotency keys

For Stripe:

import crypto from 'crypto';

async function chargeCustomer(customerId, amount) {
  const idempotencyKey = crypto.randomUUID();

  const charge = await stripe.charges.create(
    {
      amount,
      currency: 'usd',
      customer: customerId,
      description: 'Order #12345',
    },
    {
      idempotencyKey,
    }
  );

  return charge;
}

For Resend:

import crypto from 'crypto';

async function sendEmail(to, subject, html) {
  const idempotencyKey = crypto.randomUUID();

  const response = await resend.emails.send({
    from: 'noreply@example.com',
    to,
    subject,
    html,
    headers: {
      'idempotency-key': idempotencyKey,
    },
  });

  return response;
}

Frequently asked questions

Q: Should I reuse the same idempotency key? No. Generate a fresh UUID for each intended operation. Reusing a key is exactly what stops a retry from creating a duplicate, so you want one unique key per thing you actually mean to do.

Q: How long do providers cache keys? Usually 24 hours, but check your provider's docs to be sure.

Q: Do I need a library for this? Nope. Node ships crypto.randomUUID() and that's all you need.

Q: What if my request fails on a validation error? The key still applies. Retry a validation error with the same key and you'll just get the same cached error back. So don't retry validation errors, fix the input instead.

Next Steps

Try api-doctor

Deterministic AST rules for AI-generated API integrations. Not a prompt.

npx @api-doctor/cli .