Idempotency
How MintCash makes safe retries safe. Use externalId on every charge so duplicate requests never cost your customer twice.
Network calls fail. When they do, you retry. With charge endpoints that's terrifying — a retry that "succeeded last time but timed out" can double-charge a customer.
MintCash solves this with idempotency keys. Every charge endpoint takes an externalId — your own unique identifier for that order or invoice. If we receive the same externalId twice, we return the original resource and never re-charge the card.
How it works
The first request creates the payment and stores everything we'd need to return on a replay — the payment row, the provider's response, the redirectUrl. The second request, with the same externalId, never touches the provider. You get the same response back.
Where to use it
| Endpoint | Idempotency field | Required? |
|---|---|---|
POST /payments | externalId | Yes |
POST /subscriptions | externalId | Yes |
POST /payments/{id}/refund | externalId (per refund) | Yes |
What counts as a duplicate
Any request with an externalId we've already seen is treated as a replay: we return the original resource and never touch the provider again. The rest of the payload is not compared — a request that reuses an externalId with a different amount or customer still gets the original resource back, silently. Make sure an externalId maps to exactly one intent in your system; never recycle one for a different charge.
Good externalIds
Pick something that's stable, unique, and meaningful in your system:
- Your order ID:
order_2026_05_22_001 - Your invoice ID:
inv_8347 - A UUID generated when you start the checkout flow
Avoid using anything you might re-emit for a different purpose. customer_id is wrong (one per customer, infinite charges per customer). Timestamps alone are wrong (two parallel checkouts could collide).
Combine with webhook deduplication
Idempotency keys protect the request side. On the webhook side, every event has an eventId that's stable across retries — use it as a dedupe key on your handler:
async function handleWebhook(event: WebhookEvent) {
const exists = await db.processedEvents.exists(event.eventId);
if (exists) return; // already handled
await db.transaction(async (tx) => {
await fulfillOrder(event.data);
await tx.processedEvents.insert(event.eventId);
});
}Two layers, one promise
externalId on requests means we never double-charge. eventId on webhooks
means you never double-fulfil. Together, your integration is idempotent
end-to-end.