Idempotency
Network failures happen. A request can time out after leaving your server but before you receive a response — leaving you unsure whether it was processed. Idempotency lets you safely retry such requests without the risk of creating duplicate transactions.
How Capera handles idempotency
Capera uses the reference field as the idempotency key on all write operations. If you submit a request with a reference that already exists, Capera returns the original result rather than creating a new record.
This applies to:
| Endpoint | Idempotency key |
|---|---|
POST /v1/transfers/initiate | reference |
POST /v1/momo/transfer | reference |
POST /v1/momo/collection | reference |
The safe retry pattern
1. Always check before retrying
If a request times out or returns a 5xx error, check whether it succeeded before retrying:
# Check if the transfer was created
curl https://api.withcapera.com/b2b/v1/transfers/TRF-2024-001 \
-H "Authorization: Bearer $CAPERA_API_KEY"- If the response returns the transfer → it was created. Do not retry.
- If you get
400with "Transfer not found" → it was not created. Safe to retry with the same reference.
2. Retry with the same reference
Retrying with the same reference is safe — the original request will be returned:
curl -X POST https://api.withcapera.com/b2b/v1/transfers/initiate \
-H "Authorization: Bearer $CAPERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference": "TRF-2024-001",
"amount": 500000,
"bankSlug": "first-bank",
"accountNumber": "1234567890",
"accountName": "Amara Okafor"
}'If TRF-2024-001 already exists, you get back the original result. No duplicate transfer is created.
Generating good references
A reference must be unique per business. Collisions cause 409 Conflict errors. Use a format that is deterministic for the underlying intent — so a retry naturally produces the same value.
Good patterns:
TRF-{orderId} → TRF-1042
TRF-{orderId}-{attemptNumber} → TRF-1042-1
{date}-{uuid} → 2024-01-15-f47ac10b-58cc
PAYOUT-{payrollId}-{employeeId} → PAYOUT-JAN24-EMP009
Avoid using timestamps alone as references — if you generate a new timestamp on retry, you lose idempotency.
Conflict errors
Submitting a request that conflicts with an existing record in a way that cannot be resolved returns 409 Conflict:
{
"success": false,
"error": {
"code": "CONFLICT",
"message": "Customer with this reference already exists"
},
"requestId": "req_abc123def456",
"timestamp": "2024-01-15T10:00:00Z"
}409 means the conflict is permanent — retrying the same request will not resolve it. You need to either use a different reference or handle the existing record.
Exponential backoff for server errors
For 500 or 503 responses, retry with exponential backoff:
async function withRetry(fn, maxAttempts = 4) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxAttempts || err.status < 500) throw err;
const delay = Math.min(1000 * 2 ** attempt, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Do not retry 4xx errors — they indicate a problem with the request itself that will not resolve on its own.
Summary
| Situation | Action |
|---|---|
| Request timed out | Check by reference first, then retry with the same reference if not found |
Got 5xx response | Retry with exponential backoff using the same reference |
Got 409 Conflict | Do not retry — handle the existing record or use a new reference |
Got 4xx (not 409) | Do not retry — fix the request |
Updated 4 months ago