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:

EndpointIdempotency key
POST /v1/transfers/initiatereference
POST /v1/momo/transferreference
POST /v1/momo/collectionreference

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 400 with "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

SituationAction
Request timed outCheck by reference first, then retry with the same reference if not found
Got 5xx responseRetry with exponential backoff using the same reference
Got 409 ConflictDo not retry — handle the existing record or use a new reference
Got 4xx (not 409)Do not retry — fix the request

Did this page help you?