Idempotency
Idempotency ensures that retrying a request produces the same result as the original request, preventing duplicate resource creation. This is essential for payment operations where network failures or timeouts may require request retries.
Overview
An idempotency key is a unique identifier included in the request header. When Martis receives a request with an idempotency key:
| Scenario | Behavior |
|---|---|
| First request | Processed normally; response cached against the key |
| Retry with same key and payload | Returns the cached response without reprocessing |
| Same key with different payload | Returns a 409 Conflict error |
Idempotency keys are valid for 24 hours after the initial request.
Implementation
Include the Idempotency-Key header in POST requests to supported endpoints:
Request with idempotency key
curl https://api-staging.martis.id/api/v1/payments/charges \
--request POST \
--header 'Authorization: Bearer {API_KEY}' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order_12345_payment_v1' \
--data '{
"payment_method": "qris",
"channel": "gudang_voucher",
"amount": 50000,
"currency": "idr",
"customer_details": {
"email": "customer@example.com"
}
}'
Supported Endpoints
Idempotency is supported on all POST endpoints that create resources:
| Endpoint | Resource |
|---|---|
POST /v1/payments/charges | Payment charges |
POST /v1/payouts | Payouts |
POST /v1/transfers | Transfers |
GET, PUT, and DELETE requests are inherently idempotent and do not require idempotency keys.
Key Generation Strategies
Generate idempotency keys that are unique per logical operation and stable across retries.
Recommended Approaches
| Strategy | Example | Use Case |
|---|---|---|
| UUID | 550e8400-e29b-41d4-a716-446655440000 | General purpose |
| Business identifier | order_12345_payment | Tied to business objects |
| Composite key | user_123_invoice_456_attempt_1 | Multi-entity operations |
| Hash-based | SHA256 of operation details | Deterministic generation |
Key Format
- Name
Idempotency-Key- Type
- string
- Description
A unique string identifying the logical operation. Maximum length: 255 characters.
Best Practices
Key Generation
- Tie to business operations — Use order IDs, invoice numbers, or other business identifiers
- Avoid randomization on retry — The same operation must use the same key across retries
- Include version or attempt — Append a version if the same operation may be intentionally repeated
Storage and Tracking
- Store the mapping — Maintain a record linking business identifiers to idempotency keys
- Handle key expiration — Idempotency keys expire after 24 hours; generate new keys for operations after expiration
- Log for debugging — Record idempotency keys in logs for troubleshooting
Retry Logic
- Retry on network errors — Safe to retry with the same key when the request fails due to network issues
- Retry on 5xx responses — Server errors indicate the request may not have been processed
- Do not retry on 4xx responses — Client errors require fixing the request, not retrying
Error Handling
Idempotency Conflict (409)
Returned when the same idempotency key is used with a different request payload:
Response 409
{
"status": "fail",
"data": {
"message": "Idempotency key already used with different parameters"
}
}
Resolution: Generate a new idempotency key if the operation parameters have changed.
Successful Retry
When a request is successfully retried, the original response is returned:
Cached response returned
{
"status": "success",
"data": {
"id": "01HZCHARGE123456ABCDEF",
"payment_method": "qris",
"status": "pending"
}
}
The response is identical to the original, including the same resource ID.
Examples
Payment Charge with Idempotency
Create payment charge
curl https://api-staging.martis.id/api/v1/payments/charges \
--request POST \
--header 'Authorization: Bearer {API_KEY}' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: checkout_789_charge' \
--data '{
"payment_method": "virtual_account",
"channel": "mandiri",
"amount": 150000,
"currency": "idr",
"customer_details": {
"email": "customer@example.com",
"name": "Customer Name"
},
"client_reference_id": "checkout_789"
}'
Payout with Idempotency
Create payout
curl https://api-staging.martis.id/api/v1/payouts \
--request POST \
--header 'Authorization: Bearer {API_KEY}' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: vendor_payment_PO2024001' \
--data '{
"amount": 500000,
"currency": "idr",
"destination": {
"type": "bank_account",
"bank_account": {
"bank_code": "bca",
"account_number": "1234567890",
"account_holder_name": "Vendor Name"
}
},
"client_reference_id": "PO-2024-001"
}'
Transfer with Idempotency
Create transfer
curl https://api-staging.martis.id/api/v1/transfers \
--request POST \
--header 'Authorization: Bearer {API_KEY}' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: revenue_share_invoice_456' \
--data '{
"destination_account_id": "01HZDEF456789012ABCDEF",
"amount": 100000,
"currency": "idr",
"client_reference_id": "invoice_456_share"
}'
Retry Implementation
JavaScript Example
Retry with idempotency
async function createPaymentWithRetry(paymentData, maxRetries = 3) {
const idempotencyKey = `payment_${paymentData.orderId}_${Date.now()}`;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api-staging.martis.id/api/v1/payments/charges', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(paymentData)
});
if (response.status < 500) {
return await response.json();
}
// Retry on 5xx errors
if (attempt < maxRetries - 1) {
await sleep(Math.pow(2, attempt) * 1000);
}
} catch (error) {
// Retry on network errors
if (attempt < maxRetries - 1) {
await sleep(Math.pow(2, attempt) * 1000);
} else {
throw error;
}
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
409 Conflict on first request | Key previously used with different payload | Generate a new unique key |
| Duplicate resources created | Idempotency key not included | Add Idempotency-Key header |
| Different response on retry | Key expired (24 hours) | Generate a new key for expired operations |
| Key rejected as invalid | Key exceeds 255 characters | Shorten the key format |