Skip to main content
Version: 2019-05-29 (Current)

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.

Quick Start
  • Add Idempotency-Key header 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โ€‹

  1. You send a request with an Idempotency-Key header
  2. Omise processes it and stores the result
  3. 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โ€‹

RequirementDescription
FormatAny string up to 255 characters
UniquenessMust be unique per operation
CharactersAlphanumeric and hyphens recommended
Case Sensitivekey-1 โ‰  KEY-1
LifetimeStored for 24 hours
# 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โ€‹

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:

  • โœ… Easy to reconstruct key later
  • โœ… Links charge to your system
  • โœ… Can check if order was already charged

Cons:

  • โš ๏ธ Need unique order IDs
  • โš ๏ธ Can't retry with different parameters

Strategy 3: Store Key with Requestโ€‹

Save the key in your database:

// Store in database
const payment = await db.payments.create({
order_id: 'ORD-12345',
amount: 100000,
idempotency_key: uuidv4(),
status: 'pending'
});

// Use stored key for charge
const charge = await omise.charges.create({
amount: payment.amount,
currency: 'thb',
card: token,
headers: {
'Idempotency-Key': payment.idempotency_key
}
});

// Update status
await db.payments.update(payment.id, {
status: 'completed',
charge_id: charge.id
});

Pros:

  • โœ… Can retry anytime (key stored)
  • โœ… Audit trail
  • โœ… Can correlate requests

Cons:

  • โš ๏ธ More complex
  • โš ๏ธ Requires database

Strategy 4: Hash of Request Parametersโ€‹

Generate key from request content:

<?php
function getIdempotencyKey($chargeParams) {
$content = json_encode($chargeParams);
return hash('sha256', $content);
}

$chargeParams = [
'amount' => 100000,
'currency' => 'thb',
'card' => $token,
'metadata' => ['order_id' => 'ORD-12345']
];

$idempotencyKey = getIdempotencyKey($chargeParams);

$charge = OmiseCharge::create($chargeParams, [
'Idempotency-Key' => $idempotencyKey
]);

Pros:

  • โœ… Same parameters = same key automatically
  • โœ… No storage needed
  • โœ… Deterministic

Cons:

  • โš ๏ธ Different parameters = different key (might want same charge)
  • โš ๏ธ Sensitive to parameter order
  • โš ๏ธ Token changes would change key

Key Expirationโ€‹

24-Hour Lifetimeโ€‹

Idempotency keys are stored for 24 hours from first use:

# Day 1, 10:00 AM - First request
curl -H "Idempotency-Key: abc123" ...
# Creates new charge, key stored until Day 2, 10:00 AM

# Day 1, 11:00 AM - Retry
curl -H "Idempotency-Key: abc123" ...
# Returns cached result from 10:00 AM

# Day 2, 11:00 AM - After expiration
curl -H "Idempotency-Key: abc123" ...
# Key expired, creates NEW charge (different from first)
Key Expiration

After 24 hours, the same key will create a new resource. Don't reuse keys across different operations or after 24 hours.

Handling Expired Keysโ€‹

# โŒ Bad - Reusing old key
def create_charge_for_order(order_id)
# This key might be expired if order is old
key = "order-#{order_id}"

Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => key
})
end

# โœ… Good - Always use fresh key
def create_charge_for_order(order_id)
# Combine order ID with timestamp
key = "order-#{order_id}-#{Time.now.to_i}"

Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token,
metadata: { order_id: order_id }
}, {
'Idempotency-Key' => key
})
end

Retry Logic Best Practicesโ€‹

1. Retry Only Transient Errorsโ€‹

function isRetryable(error) {
// Retry server errors
if (error.statusCode >= 500) return true;

// Retry timeouts
if (error.code === 'ETIMEDOUT') return true;
if (error.code === 'ECONNRESET') return true;

// Don't retry client errors
if (error.statusCode >= 400 && error.statusCode < 500) return false;

return false;
}

async function chargeWithRetry(chargeData) {
const idempotencyKey = uuidv4();
const maxRetries = 3;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});

} catch (error) {
if (!isRetryable(error) || attempt === maxRetries - 1) {
throw error;
}

// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

2. Use Exponential Backoffโ€‹

import time
import random

def exponential_backoff(attempt, base_delay=1, max_delay=60):
"""Calculate delay with jitter"""
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # Add 10% jitter
return delay + jitter

def create_charge_with_retry(charge_data, max_attempts=5):
idempotency_key = str(uuid.uuid4())

for attempt in range(max_attempts):
try:
return omise.Charge.create(
**charge_data,
headers={'Idempotency-Key': idempotency_key}
)

except omise.errors.BaseError as e:
is_last_attempt = attempt == max_attempts - 1

if e.http_status < 500 or is_last_attempt:
raise

# Wait before retry
delay = exponential_backoff(attempt)
time.sleep(delay)

raise Exception("Max retries exceeded")

3. Set Timeoutsโ€‹

require 'timeout'

def create_charge_with_timeout(charge_params, timeout_seconds: 30)
idempotency_key = SecureRandom.uuid

begin
Timeout.timeout(timeout_seconds) do
Omise::Charge.create(
charge_params,
{ 'Idempotency-Key' => idempotency_key }
)
end

rescue Timeout::Error
# Timeout - safe to retry with same key
retry_charge(charge_params, idempotency_key)
end
end

def retry_charge(params, key, max_retries: 3)
max_retries.times do |attempt|
begin
return Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
rescue Omise::Error => e
raise unless e.http_status >= 500
sleep(2 ** attempt)
end
end

raise "Max retries exceeded"
end

4. Log Retry Attemptsโ€‹

const logger = require('./logger');

async function createChargeWithLogging(chargeData) {
const idempotencyKey = uuidv4();
const maxRetries = 3;

logger.info('Creating charge', {
idempotency_key: idempotencyKey,
amount: chargeData.amount,
currency: chargeData.currency
});

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const charge = await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});

logger.info('Charge created successfully', {
idempotency_key: idempotencyKey,
charge_id: charge.id,
attempt: attempt + 1
});

return charge;

} catch (error) {
logger.error('Charge attempt failed', {
idempotency_key: idempotencyKey,
attempt: attempt + 1,
error_code: error.code,
error_message: error.message,
status_code: error.statusCode
});

const isRetryable = error.statusCode >= 500;
const isLastAttempt = attempt === maxRetries - 1;

if (!isRetryable || isLastAttempt) {
logger.error('Charge failed permanently', {
idempotency_key: idempotencyKey,
total_attempts: attempt + 1
});
throw error;
}

const delay = Math.pow(2, attempt) * 1000;
logger.info('Retrying charge', {
idempotency_key: idempotencyKey,
delay_ms: delay,
next_attempt: attempt + 2
});

await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

Advanced Patternsโ€‹

Pattern 1: Distributed Systemsโ€‹

When multiple servers might retry the same operation:

# Use Redis to coordinate retries across servers
require 'redis'

class IdempotentChargeCreator
def initialize(redis_client)
@redis = redis_client
end

def create_charge(order_id, charge_params)
# Try to acquire lock for this order
lock_key = "charge_lock:#{order_id}"
idempotency_key = "charge:#{order_id}"

# Try to set lock (expires in 60 seconds)
locked = @redis.set(lock_key, "locked", nx: true, ex: 60)

unless locked
# Another server is processing this
return wait_for_charge(order_id)
end

begin
# We got the lock - create charge
charge = Omise::Charge.create(
charge_params,
{ 'Idempotency-Key' => idempotency_key }
)

# Store result
@redis.set("charge_result:#{order_id}", charge.to_json, ex: 86400)

charge

ensure
# Release lock
@redis.del(lock_key)
end
end

private

def wait_for_charge(order_id)
# Wait for other server to finish
10.times do
result = @redis.get("charge_result:#{order_id}")
return JSON.parse(result) if result

sleep(0.5)
end

raise "Timeout waiting for charge"
end
end

Pattern 2: Webhook Idempotencyโ€‹

Handle duplicate webhook deliveries:

# Webhooks can be delivered multiple times
# Use event ID as idempotency key

processed_events = set() # Or use database/Redis

def handle_webhook(event_data):
event_id = event_data['id']

# Check if already processed
if event_id in processed_events:
print(f"Event {event_id} already processed")
return

# Process event
if event_data['key'] == 'charge.complete':
process_successful_charge(event_data['data'])

# Mark as processed
processed_events.add(event_id)

# Better: Store in database
def handle_webhook_persistent(event_data):
event_id = event_data['id']

# Try to insert (unique constraint on event_id)
try:
db.events.create({
'event_id': event_id,
'processed_at': datetime.now()
})
except UniqueConstraintError:
# Already processed
return

# Process event
process_event(event_data)

Pattern 3: Background Job Retriesโ€‹

Idempotent job processing:

// Using job queue (e.g., Bull, Sidekiq)
const queue = require('./queue');

queue.process('create-charge', async (job) => {
const { order_id, amount, currency, token } = job.data;

// Use job ID as part of idempotency key
const idempotencyKey = `job-${job.id}-order-${order_id}`;

try {
const charge = await omise.charges.create({
amount,
currency,
card: token,
metadata: { order_id },
headers: { 'Idempotency-Key': idempotencyKey }
});

// Store result
await db.orders.update(order_id, {
charge_id: charge.id,
status: 'charged'
});

return { success: true, charge_id: charge.id };

} catch (error) {
if (error.statusCode >= 500) {
// Retryable - job queue will retry
throw error;
} else {
// Not retryable - mark order as failed
await db.orders.update(order_id, {
status: 'failed',
error: error.message
});

return { success: false, error: error.message };
}
}
});

// Add job
queue.add('create-charge', {
order_id: 'ORD-12345',
amount: 100000,
currency: 'thb',
token: 'tokn_test_...'
}, {
attempts: 5,
backoff: {
type: 'exponential',
delay: 2000
}
});

Testing Idempotencyโ€‹

Test Duplicate Requestsโ€‹

# Test that duplicate requests return same result
describe 'Idempotency' do
it 'returns same charge for duplicate requests' do
idempotency_key = SecureRandom.uuid

# First request
charge1 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})

# Duplicate request
charge2 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})

# Should be the same charge
expect(charge2.id).to eq(charge1.id)
expect(charge2.amount).to eq(charge1.amount)
end

it 'creates new charge with different key' do
# First request
charge1 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => SecureRandom.uuid
})

# Different key
charge2 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => SecureRandom.uuid
})

# Should be different charges
expect(charge2.id).not_to eq(charge1.id)
end
end

Simulate Network Errorsโ€‹

// Test retry logic
const nock = require('nock');

describe('Idempotent Retry', () => {
it('retries on server error', async () => {
const idempotencyKey = uuidv4();

// First request fails
nock('https://api.omise.co')
.post('/charges')
.matchHeader('Idempotency-Key', idempotencyKey)
.reply(500, { error: 'Internal Server Error' });

// Retry succeeds
nock('https://api.omise.co')
.post('/charges')
.matchHeader('Idempotency-Key', idempotencyKey)
.reply(200, {
object: 'charge',
id: 'chrg_test_123',
amount: 100000
});

const charge = await createChargeWithRetry({
amount: 100000,
currency: 'thb',
card: 'tokn_test_...'
});

expect(charge.id).toBe('chrg_test_123');
});
});

Common Mistakesโ€‹

โŒ Don't: Reuse Keys Across Operationsโ€‹

# โŒ Bad - Same key for different operations
idempotency_key = "my-key"

charge1 = omise.Charge.create(
amount=100000,
headers={'Idempotency-Key': idempotency_key}
)

charge2 = omise.Charge.create(
amount=50000, # Different amount!
headers={'Idempotency-Key': idempotency_key}
)
# charge2 will return charge1's result!
# โœ… Good - Unique key per operation
charge1 = omise.Charge.create(
amount=100000,
headers={'Idempotency-Key': str(uuid.uuid4())}
)

charge2 = omise.Charge.create(
amount=50000,
headers={'Idempotency-Key': str(uuid.uuid4())}
)

โŒ Don't: Forget to Handle Cached Errorsโ€‹

# โŒ Bad - Original request failed, retry returns same error
idempotency_key = SecureRandom.uuid

begin
Omise::Charge.create({
amount: 100, # Too small!
currency: 'thb'
}, {
'Idempotency-Key' => idempotency_key
})
rescue Omise::Error => e
# Retry with fixed amount but same key
Omise::Charge.create({
amount: 100000, # Fixed amount
currency: 'thb'
}, {
'Idempotency-Key' => idempotency_key # Same key = cached error!
})
# Returns same error, not new charge
end
# โœ… Good - Use new key for corrected request
begin
Omise::Charge.create({
amount: 100,
currency: 'thb'
}, {
'Idempotency-Key' => SecureRandom.uuid
})
rescue Omise::Error => e
# Use NEW key for fixed request
Omise::Charge.create({
amount: 100000,
currency: 'thb'
}, {
'Idempotency-Key' => SecureRandom.uuid # New key!
})
end

โŒ Don't: Retry Client Errorsโ€‹

// โŒ Bad - Retrying invalid request
async function badRetry() {
const idempotencyKey = uuidv4();

for (let i = 0; i < 3; i++) {
try {
return await omise.charges.create({
amount: 100, // Too small - will always fail!
currency: 'thb',
headers: { 'Idempotency-Key': idempotencyKey }
});
} catch (error) {
// Retrying won't help - request is invalid
continue;
}
}
}

// โœ… Good - Only retry server errors
async function goodRetry(chargeData) {
const idempotencyKey = uuidv4();

for (let i = 0; i < 3; i++) {
try {
return await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});
} catch (error) {
// Only retry server errors
if (error.statusCode < 500) {
throw error; // Don't retry client errors
}
// Continue retry loop for server errors
}
}
}

Quick Referenceโ€‹

Idempotency Headerโ€‹

Idempotency-Key: <unique-string>

When to Useโ€‹

Request TypeUse Idempotency?
POST (create)โœ… Always
PATCH (update)โœ… Recommended
GET (read)โŒ Not needed
DELETEโŒ Not needed

Key Lifetimeโ€‹

  • Stored: 24 hours from first use
  • Expired: Creates new resource after 24 hours

Retry Decision Treeโ€‹

Request failed?
โ”œโ”€ Yes โ†’ Check error type
โ”‚ โ”œโ”€ 5xx Server Error โ†’ Retry with SAME key
โ”‚ โ”œโ”€ Network timeout โ†’ Retry with SAME key
โ”‚ โ”œโ”€ 4xx Client Error โ†’ Fix request, use NEW key
โ”‚ โ””โ”€ Other error โ†’ Don't retry
โ””โ”€ No โ†’ Success!

Example Implementationโ€‹

def create_charge_idempotently(params)
key = SecureRandom.uuid

Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
rescue Omise::Error => e
raise unless e.http_status >= 500

# Retry with same key
Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
end


Next: Learn about API Versioning to manage version changes and maintain backward compatibility.