The INDX Customer API supports idempotency for POST requests to prevent duplicate operations when retrying failed requests. This allows you to safely retransmit the same request with confidence that the server will execute the operation only once.
Idempotency is supported for transfers and wire withdrawals (any POST operations via the INDX API).
How Idempotency Works
Idempotency is implemented using the optional X-IDEMPOTENCY-KEY header:
- Include a unique UUID in the
X-IDEMPOTENCY-KEY header when making a POST request
- If the request fails or times out, retry with the same idempotency key
- The server will execute the operation only once, even if you send multiple requests
- Duplicate requests within 24 hours return a 409 conflict response
This ensures:
- Safe retry logic for network failures or timeouts
- Prevention of duplicate transfers or withdrawals
- Idempotent behavior for critical financial operations
Using the Idempotency Key
The X-IDEMPOTENCY-KEY header is optional. When provided, it must be a valid UUID (RFC 9562 format).
Generating an Idempotency Key
import uuid
# Generate a unique idempotency key for this request
idempotency_key = str(uuid.uuid4())
# Example: "69de51e7-c587-44ce-a4e2-2f6ec330bfdf"
Making an Idempotent Request
import requests
import uuid
url = "https://api.indx.com/customer/transfer"
headers = {
"X-API-KEY": api_key,
"X-API-SIGNATURE": signature_b64,
"X-API-TIMESTAMP": timestamp,
"X-API-NONCE": nonce,
"X-IDEMPOTENCY-KEY": str(uuid.uuid4()), # Add idempotency key
"Content-Type": "application/json"
}
data = {
"from_account": "acc_123456",
"to_account": "acc_789012",
"amount": 100.00,
"currency": "USD"
}
response = requests.post(url, headers=headers, json=data)
Retry Behavior
Successful First Request
When a request with a new idempotency key succeeds:
// HTTP 201 Created
{
"status": "success",
"data": {
"transfer_id": "txn_abc123",
"amount": 100.00,
"status": "completed"
}
}
Duplicate Request Detection
If you retry the same request with the same idempotency key within 24 hours:
// HTTP 409 Conflict
{
"message": "request conflict"
}
This indicates the original operation already succeeded, and no duplicate was created.
Complete Retry Example
import requests
import uuid
import time
def make_transfer_with_retry(from_account, to_account, amount, max_retries=3):
"""
Make a transfer with automatic retry logic using idempotency.
"""
# Generate idempotency key once for all retry attempts
idempotency_key = str(uuid.uuid4())
for attempt in range(max_retries):
try:
url = "https://api.indx.com/customer/transfer"
# Generate fresh authentication headers for each attempt
timestamp = str(int(time.time() * 1000))
nonce = str(uuid.uuid4())
# ... generate signature ...
headers = {
"X-API-KEY": API_KEY,
"X-API-SIGNATURE": signature_b64,
"X-API-TIMESTAMP": timestamp,
"X-API-NONCE": nonce,
"X-IDEMPOTENCY-KEY": idempotency_key, # Same key for all retries
"Content-Type": "application/json"
}
data = {
"from_account": from_account,
"to_account": to_account,
"amount": amount,
"currency": "USD"
}
response = requests.post(url, headers=headers, json=data, timeout=30)
if response.status_code == 201:
# Success - operation completed
return response.json()
elif response.status_code == 409:
# Conflict - operation already completed in previous attempt
print("Transfer already processed")
return {"status": "duplicate", "message": "request conflict"}
elif response.status_code == 400:
# Bad request - don't retry
raise ValueError(f"Invalid request: {response.json()}")
else:
# Other error - retry
print(f"Attempt {attempt + 1} failed: {response.status_code}")
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
continue
else:
raise Exception(f"Transfer failed after {max_retries} attempts")
except requests.exceptions.Timeout:
print(f"Attempt {attempt + 1} timed out")
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
else:
raise
except requests.exceptions.RequestException as e:
print(f"Network error on attempt {attempt + 1}: {e}")
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
else:
raise
# Usage
transfer_result = make_transfer_with_retry("acc_123456", "acc_789012", 100.00)
print(transfer_result)
Error Responses
If the X-IDEMPOTENCY-KEY header contains an invalid UUID:
// HTTP 400 Bad Request
{
"message": "invalid UUID passed as x-idempotency-key"
}
Valid UUID formats include:
69de51e7-c587-44ce-a4e2-2f6ec330bfdf (UUID v4 with hyphens)
69de51e7c58744cea4e22f6ec330bfdf (UUID without hyphens)
Best Practices
Generate Idempotency Keys Client-Side
Always generate UUIDs on the client before making requests:
import uuid
# Generate once per logical operation
idempotency_key = str(uuid.uuid4())
# Use the same key for all retry attempts of this operation
Use Idempotency for All POST Requests
Include idempotency keys for all transfers and wire withdrawals to ensure safe retry behavior:
# Always include for financial operations
headers["X-IDEMPOTENCY-KEY"] = str(uuid.uuid4())
Store Keys for Audit Trail
Log idempotency keys with your transactions for debugging and reconciliation:
logger.info(f"Initiating transfer with idempotency key: {idempotency_key}")
Handle 409 Responses Appropriately
A 409 response means the operation already succeeded:
if response.status_code == 409:
# Don't treat this as an error - the operation completed
print("Transfer already processed successfully")
Idempotency Key Expiration
Idempotency keys are valid for 24 hours. After 24 hours:
- The same key can be reused (though this is not recommended)
- A new request with the old key will be processed as a new operation
Without Idempotency Keys
Requests without the X-IDEMPOTENCY-KEY header will process normally:
- No duplicate detection occurs
- Each request creates a new operation
- Retrying may result in duplicate transfers
Without idempotency keys, network failures or timeouts may result in duplicate operations if you retry requests. Always use idempotency keys for transfers and withdrawals.