Idempotency
Safely retry API requests without creating duplicate charges, customers, or other resources. Learn how to use idempotency keys to build reliable payment integrations.
Overviewโ
Network issues, timeouts, and server errors can cause API requests to fail or return unclear results. Idempotency allows you to safely retry requests without worrying about duplicate operations. By providing an Idempotency-Key header, Omise guarantees the same request will produce the same result, even if sent multiple times.
- Add
Idempotency-Keyheader to POST/PATCH requests - Use unique key per operation (UUID recommended)
- Same key returns same result (cached for 24 hours)
- Essential for charge creation and money operations
- Prevents duplicate payments during network issues
What is Idempotency?โ
Idempotency means an operation can be performed multiple times with the same result. In payment processing, this is critical:
Without Idempotencyโ
1. Send charge request โ Network timeout
2. Did it succeed? Unknown. Retry?
3. Retry โ Duplicate charge! Customer charged twice ๐ฅ
With Idempotencyโ
1. Send charge with idempotency key โ Network timeout
2. Retry with same key โ Same result returned
3. No duplicate charge โ
How Idempotency Worksโ
- You send a request with an
Idempotency-Keyheader - Omise processes it and stores the result
- If you retry with the same key within 24 hours:
- Omise returns the cached result
- No new operation is performed
- Same response status code and body
Example Flowโ
# First request (network timeout)
curl https://api.omise.co/charges \
-X POST \
-u skey_test_...: \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d "amount=100000" \
-d "currency=thb" \
-d "card=tokn_test_..."
# Response: (timeout - unclear if succeeded)
# Retry with same key
curl https://api.omise.co/charges \
-X POST \
-u skey_test_...: \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d "amount=100000" \
-d "currency=thb" \
-d "card=tokn_test_..."
# Response: Returns the original charge (not a new one)
When to Use Idempotencyโ
Always Use For:โ
โ Creating Charges
POST /charges
Most important - prevents duplicate payments
โ Creating Customers
POST /customers
Prevents duplicate customer records
โ Creating Refunds
POST /charges/:id/refunds
Prevents duplicate refunds
โ Creating Transfers
POST /transfers
Prevents duplicate payouts
โ Creating Recipients
POST /recipients
Prevents duplicate recipient records
โ Any POST Request All POST requests that create resources should use idempotency keys
โ PATCH Requests Updates can be retried safely with idempotency
Not Needed For:โ
โ GET Requests Reading data is already idempotent (no side effects)
โ DELETE Requests Deleting is naturally idempotent (deleting twice = same result)
Idempotency-Key Headerโ
Header Formatโ
Idempotency-Key: <unique-string>
Key Requirementsโ
| Requirement | Description |
|---|---|
| Format | Any string up to 255 characters |
| Uniqueness | Must be unique per operation |
| Characters | Alphanumeric and hyphens recommended |
| Case Sensitive | key-1 โ KEY-1 |
| Lifetime | Stored for 24 hours |
Recommended: Use UUIDsโ
# UUIDv4 format (recommended)
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Why UUIDs?
- โ Guaranteed uniqueness
- โ No collision risk
- โ Standard format
- โ Available in all languages
Implementation Examplesโ
Rubyโ
require 'omise'
require 'securerandom'
Omise.api_key = ENV['OMISE_SECRET_KEY']
# Generate unique idempotency key
idempotency_key = SecureRandom.uuid
begin
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})
puts "Charge created: #{charge.id}"
rescue Omise::Error => e
if e.http_status >= 500 || e.message.include?('timeout')
# Safe to retry with same key
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key # Same key!
})
else
raise
end
end
Pythonโ
import omise
import uuid
omise.api_secret = os.environ['OMISE_SECRET_KEY']
# Generate unique idempotency key
idempotency_key = str(uuid.uuid4())
try:
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key}
)
print(f"Charge created: {charge.id}")
except omise.errors.BaseError as e:
if e.http_status >= 500:
# Safe to retry with same key
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key} # Same key!
)
else:
raise
PHPโ
<?php
require_once 'vendor/autoload.php';
define('OMISE_SECRET_KEY', getenv('OMISE_SECRET_KEY'));
// Generate unique idempotency key
$idempotencyKey = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
try {
$charge = OmiseCharge::create([
'amount' => 100000,
'currency' => 'thb',
'card' => $token
], [
'Idempotency-Key' => $idempotencyKey
]);
echo "Charge created: " . $charge['id'];
} catch (Exception $e) {
if ($e->getCode() >= 500) {
// Retry with same key
$charge = OmiseCharge::create([
'amount' => 100000,
'currency' => 'thb',
'card' => $token
], [
'Idempotency-Key' => $idempotencyKey // Same key!
]);
} else {
throw $e;
}
}
Node.jsโ
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const { v4: uuidv4 } = require('uuid');
async function createChargeWithRetry(chargeData, maxRetries = 3) {
// Generate unique idempotency key
const idempotencyKey = uuidv4();
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const charge = await omise.charges.create({
...chargeData,
headers: {
'Idempotency-Key': idempotencyKey
}
});
console.log(`Charge created: ${charge.id}`);
return charge;
} catch (error) {
const isServerError = error.statusCode >= 500;
const isTimeout = error.code === 'ETIMEDOUT';
const isLastAttempt = attempt === maxRetries - 1;
if ((isServerError || isTimeout) && !isLastAttempt) {
// Safe to retry with same key
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
// Usage
createChargeWithRetry({
amount: 100000,
currency: 'thb',
card: 'tokn_test_...'
});
Goโ
package main
import (
"fmt"
"github.com/google/uuid"
"github.com/omise/omise-go"
"github.com/omise/omise-go/operations"
"time"
)
func createChargeWithRetry(client *omise.Client, amount int64, currency, token string) (*omise.Charge, error) {
// Generate unique idempotency key
idempotencyKey := uuid.New().String()
maxRetries := 3
for attempt := 0; attempt < maxRetries; attempt++ {
charge, err := client.CreateCharge(&operations.CreateCharge{
Amount: amount,
Currency: currency,
Card: token,
Headers: map[string]string{
"Idempotency-Key": idempotencyKey,
},
})
if err == nil {
return charge, nil
}
// Check if retryable
if omiseErr, ok := err.(*omise.Error); ok {
if omiseErr.StatusCode >= 500 && attempt < maxRetries-1 {
// Retry with exponential backoff
delay := time.Duration(1<<uint(attempt)) * time.Second
time.Sleep(delay)
continue
}
}
return nil, err
}
return nil, fmt.Errorf("max retries exceeded")
}
func main() {
client, _ := omise.NewClient(
os.Getenv("OMISE_PUBLIC_KEY"),
os.Getenv("OMISE_SECRET_KEY"),
)
charge, err := createChargeWithRetry(client, 100000, "thb", "tokn_test_...")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Charge created: %s\n", charge.ID)
}
Idempotency Key Strategiesโ
Strategy 1: UUID per Request (Recommended)โ
Generate a new UUID for each unique operation:
# Each charge gets unique key
charge1_key = SecureRandom.uuid
charge1 = create_charge(amount: 100_000, key: charge1_key)
charge2_key = SecureRandom.uuid
charge2 = create_charge(amount: 50_000, key: charge2_key)
Pros:
- โ Simple and safe
- โ No collision risk
- โ Works for all scenarios
Cons:
- โ ๏ธ Need to store key if you want to check status later
Strategy 2: Derived from Order IDโ
Base key on your internal order/transaction ID:
def get_idempotency_key(order_id):
return f"order-{order_id}"
# Create charge for order
order_id = "ORD-12345"
idempotency_key = get_idempotency_key(order_id)
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
metadata={'order_id': order_id},
headers={'Idempotency-Key': idempotency_key}
)
Pros: