Idempotency Guidelines
Idempotency ensures that repeating the same API request produces the same result without duplicating operations. This is critical for payment operations where network failures, timeouts, or retries could otherwise create duplicate transactions.
Why Idempotency Matters
In distributed systems, requests can fail or timeout without you knowing whether they succeeded:
- Network timeout: Request succeeded but response was lost
- Server error: Request partially processed before failure
- Client retry: User clicks submit button multiple times
Without idempotency, retrying these requests could create duplicate payments, causing financial discrepancies and poor user experience.
How It Works
- Generate a unique key for each intended operation (UUID v4 recommended)
- Include the key in the
Idempotency-Keyheader of your POST request - Server checks if it has seen this key before:
- If new: processes the request and stores the result with the key
- If seen with same body: returns the stored result
- If seen with different body: returns
409 IDEMPOTENCY_CONFLICT
- Safe to retry the same request if you don’t receive a response
Important: Keys are valid for 24 hours. After this period, the same key can be used for a new request.
Required Headers
POST /api/v1/pay-in/deposit-creationContent-Type: application/jsonAuthorization: Basic {base64(businessKey:businessSecret)}Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000| Header | Required | Description |
|---|---|---|
Idempotency-Key | Yes (POST) | Unique identifier for this operation. UUID v4 format recommended |
Generating Idempotency Keys
Recommended: UUID v4
Use a UUID v4 generator built into your programming language:
const { randomUUID } = require('crypto');const idempotencyKey = randomUUID();// Output: "550e8400-e29b-41d4-a716-446655440000"# Pythonimport uuididempotency_key = str(uuid.uuid4())# Output: "550e8400-e29b-41d4-a716-446655440000"// PHP$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));// Or use: Ramsey\Uuid\Uuid::uuid4()->toString();Alternative: Deterministic Keys
For certain use cases, you may want keys that are reproducible based on your business logic:
// Deterministic key based on operation contextconst crypto = require('crypto');
function generateDeterministicKey(userId, action, timestamp) { const data = `${userId}:${action}:${timestamp}`; return crypto.createHash('sha256').update(data).digest('hex');}
// Example: same inputs always produce same keyconst key = generateDeterministicKey('user_123', 'deposit', '2024-01-15T10:30:00Z');Example: Safe Retry Pattern
async function createDepositWithRetry(depositData, maxRetries = 3) { // Generate key ONCE for all retry attempts const idempotencyKey = crypto.randomUUID();
for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch('https://api.cashela.com/api/v1/pay-in/deposit-creation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${Buffer.from(`${BUSINESS_KEY}:${BUSINESS_SECRET}`).toString('base64')}`, 'Idempotency-Key': idempotencyKey // Same key on every retry }, body: JSON.stringify(depositData) });
const result = await response.json();
if (response.ok && result.success) { return result.data; }
// Check if error is retryable if (result.error?.code === 'RATE_LIMITED' || response.status >= 500) { if (attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff await sleep(delay); continue; } }
throw new Error(`API Error: ${result.error?.message || 'Unknown error'}`);
} catch (networkError) { // Network error - safe to retry with same idempotency key if (attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; await sleep(delay); continue; } throw networkError; } }}
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}Error Responses
409 Conflict - Idempotency Key Reused with Different Body
{ "success": false, "error": { "code": "IDEMPOTENCY_CONFLICT", "message": "Idempotency key has already been used with a different request body", "details": { "original_request_id": "req_01HJ3KBCD8E9F0G1H2I3J4K5L6", "key": "550e8400-e29b-41d4-a716-446655440000" } }, "request_id": "req_01HJ3KBCD8E9F0G1H2I3J4K5L7"}Resolution: Generate a new idempotency key for the new request, or send the exact same request body.
400 Bad Request - Missing or Invalid Key
{ "success": false, "error": { "code": "INVALID_IDEMPOTENCY_KEY", "message": "Idempotency-Key header is required and must be a valid UUID v4" }, "request_id": "req_01HJ3KBCD8E9F0G1H2I3J4K5L8"}Best Practices
✅ Do
-
Generate a new key for each unique operation
// Good: New key for each depositconst depositKey = crypto.randomUUID(); -
Store key-response mappings for reconciliation
// Store in your databaseawait db.idempotencyLogs.create({key: idempotencyKey,operation: 'deposit',requestBody: depositData,responseId: result.data.id,createdAt: new Date()}); -
Reuse the same key when retrying after timeout/error
// Key is generated once, outside the retry loopconst idempotencyKey = crypto.randomUUID();for (let attempt = 0; attempt < maxRetries; attempt++) {// Use same idempotencyKey for all attempts} -
Include idempotency key in all POST requests
- Deposit creation
- Transaction creation
- Payout initiation
❌ Don’t
-
Don’t generate a new key on each retry
// BAD: This defeats the purpose of idempotencyfor (let attempt = 0; attempt < maxRetries; attempt++) {const key = crypto.randomUUID(); // Wrong!await makeRequest(key);} -
Don’t use predictable or sequential keys
// BAD: Easily guessableconst key = `deposit-${userId}-${counter++}`; -
Don’t reuse keys for different operations
// BAD: Same key for different depositsconst globalKey = 'my-app-key';await createDeposit(data1, globalKey);await createDeposit(data2, globalKey); // Will fail!
Implementation Checklist
- Generate UUID v4 keys for all POST requests
- Store idempotency keys with request/response data
- Implement retry logic that reuses the same key
- Handle
IDEMPOTENCY_CONFLICTerrors appropriately - Log idempotency keys for debugging and support
- Set up monitoring for idempotency-related errors
Related Documentation
- Error Handling – Complete error codes and handling
- PayIn Create Deposit – Deposit creation with idempotency
- PayOut Transaction – Payout transactions with idempotency