I Clicked Pay Twice, Will I Be Charged Twice?
Learn how idempotency keys prevent duplicate charges, orders, and other side effects when retrying API requests. Includes a practical Go example you can run yourself.
You click "Pay Now." The page hangs. You click again. Did you just pay twice?
How does the server know the difference between a double-click and two separate payments? What happens when your app retries after a timeout, or a network glitch sends the same request multiple times?
The Problem: Without protection, you could be charged twice (or more)!
The answer is idempotency keys—a simple concept that every payment API and critical service uses to prevent duplicate actions. Once you understand it, you'll see it everywhere.
Quick Summary:
Idempotency keys help your APIs remember that a request was already done — so even if you click 'Pay' ten times, you're charged only once. You send a unique key with your request, and the server uses it to prevent duplicate processing. If it's seen the key before, it returns the cached result instead of running the operation again.
What's an Idempotency Key?
An idempotency key is a unique identifier you generate on the client side and send with your API request.
Think of it like a receipt number: you include it in a special header:
Idempotency-Key: payment-abc123
The server uses this key to remember if it's already processed your request. If you retry with the same key, it returns the cached result instead of processing again.
This means one payment operation, even if you retry multiple times.
When to Use It
Use idempotency keys for operations that have side effects and shouldn't be duplicated:
Critical Use Cases:
- Payments — prevent double-charging
- Order creation — avoid duplicate orders
- Account creation — prevent duplicate accounts
- Email sending — avoid sending the same email twice
High Level Working
From the client's perspective, here's what happens:
The Client Flow:
1. Generate a unique key → "payment-abc123"
2. Send request with Idempotency-Key header
3. Request fails? Retry with the SAME key
4. Server recognizes the key → returns cached result (no duplicate charge!)
Go Example
Here's a complete example showing how to implement idempotency keys in your client code:
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"time"
)
// generateIdempotencyKey creates a unique key for this operation
// Below uses a simple approach consisting of operation + random bytes as an example
func generateIdempotencyKey(operation string) string {
bytes := make([]byte, 8)
rand.Read(bytes)
return fmt.Sprintf("%s-%s", operation, hex.EncodeToString(bytes))
}
func makePayment(amount int) {
key := generateIdempotencyKey("payment") // same key generated and used for retries
// Retry up to 3 times in case of failure
for i := 0; i < 3; i++ {
err := sendPaymentRequest(amount, key)
if err == nil {
fmt.Println("Payment success!")
return
}
fmt.Println("Retrying...")
time.Sleep(2 * time.Second)
}
fmt.Println("Payment failed after retries")
}
func sendPaymentRequest(amount int, key string) error {
req, _ := http.NewRequest("POST", "http://localhost:8080/process", nil)
// Sets the same idempotency key in header for every retry
req.Header.Set("Idempotency-Key", key)
_, err := http.DefaultClient.Do(req)
return err
}
func main() {
// Make a payment - will retry if it fails
makePayment(100)
}
Summary
Idempotency keys are your safety net for retries.
The Golden Rule:
- Generate a unique key once per operation
- Reuse it for all retries of the same operation
- Let the server handle the rest
Even if your request times out or fails, retrying with the same key won't create duplicates—the server recognizes it and returns the cached result.
Key takeaway: One key per operation, reuse it for all retries. That's all you need to know to make your API calls safe.