Idempotency
Safely retry failed requests without creating duplicate payments.
Overview
Network failures, timeouts, and retries are inevitable. Without idempotency keys, a retry after a timeout could create a duplicate payment. With idempotency keys, retrying the exact same request is safe — the server returns the cached result from the first attempt.
Idempotency keys apply to write operations that move money:
- Sending a payment (
POST /v1/payments) - Batch payments (
POST /v1/payments/batch) - Funding a wallet (
POST /v1/wallets/:id/fund)
How to Use Idempotency Keys
SDK
Pass an idempotencyKey field in the request body:
import Wallgent from '@wallgent/sdk'
const wg = new Wallgent({ apiKey: process.env.WALLGENT_API_KEY })
const payment = await wg.payments.send({
from: 'wal_01J...',
to: 'wal_01J...',
amount: '75.00',
description: 'Invoice #1042 payment',
idempotencyKey: 'inv-1042-payment-2026-03-01',
})REST API
Pass the key as an Idempotency-Key header:
const response = await fetch('https://api.wallgent.com/v1/payments', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WALLGENT_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'inv-1042-payment-2026-03-01',
},
body: JSON.stringify({
from: 'wal_01J...',
to: 'wal_01J...',
amount: '75.00',
}),
})Key Format Recommendations
Keys must be unique per operation. Good formats:
- UUID v4:
crypto.randomUUID()— simple and universally unique - Composite key:
<userId>-<operationId>-<timestamp>— traceable to a specific operation - Hash of request:
sha256(JSON.stringify(requestBody))— deterministic for the same input
import { randomUUID } from 'node:crypto'
// Option 1: UUID (simple)
const key = randomUUID()
// Option 2: Composite (traceable)
const key = `pay-${userId}-${invoiceId}-${Date.now()}`
// Option 3: Deterministic (reproduce the same key for the same logical operation)
const key = `payroll-${payPeriod}-${employeeId}`TTL and Behavior
| Scenario | Behavior |
|---|---|
| Same key, same parameters, within 24 hours | Returns the cached response — no new operation is created |
| Same key, same parameters, after 24 hours | Creates a new operation (key has expired) |
| Same key, different parameters, any time | Returns IDEMPOTENCY_KEY_REUSE error (HTTP 409) |
The 24-hour TTL (IDEMPOTENCY_TTL_HOURS = 24) means you can safely retry within a day of the original attempt without risk of duplication.
Handling IDEMPOTENCY_KEY_REUSE
If you send a request with the same idempotency key but different parameters, the API returns an error rather than silently using the wrong cached result:
// This will return IDEMPOTENCY_KEY_REUSE (409) because the amount is different
const payment = await wg.payments.send({
from: 'wal_01J...',
to: 'wal_01J...',
amount: '100.00', // Different from the original '75.00'
idempotencyKey: 'inv-1042-payment-2026-03-01', // Same key
})Always generate a new key for a new logical operation.
Safe Retry Pattern
import { randomUUID } from 'node:crypto'
async function sendPaymentWithRetry(
from: string,
to: string,
amount: string,
description: string,
maxAttempts = 3,
) {
// Generate the idempotency key once — reuse it across all retry attempts
const idempotencyKey = randomUUID()
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const result = await wg.payments.send({
from,
to,
amount,
description,
idempotencyKey,
})
return result
} catch (err) {
const isRetryable =
err instanceof Error &&
(err.message.includes('TIMEOUT') || err.message.includes('network'))
if (!isRetryable || attempt === maxAttempts) {
throw err
}
// Exponential backoff before retry
const delay = 1000 * 2 ** (attempt - 1)
await new Promise((r) => setTimeout(r, delay))
}
}
}The key is generated once before the loop. All retry attempts use the same key, so only one payment is ever created regardless of how many requests reach the server.