Idempotency

Prevent duplicate charges with idempotency keys.

Why Idempotency?

Network failures can cause your request to reach the API but prevent you from receiving the response. Without idempotency, retrying the request could create a second, duplicate charge.

RohoPay enforces idempotency on all money-moving endpoints so a retried request never creates a duplicate charge.

  • POST /api/v1/collect and POST /api/v1/disburse — you must send a unique Idempotency-Key header on every request.
  • POST /api/v1/checkout (card payments) — send a unique Idempotency-Key header to dedupe a retried 3DS initiation.
  • Wallet card top-ups, dashboard withdrawals, and public/digital checkout — RohoPay derives a stable idempotency key server-side (keyed on the wallet + request type), so browser retries after a network drop are automatically deduplicated even though the client never sends a key.

How It Works

  1. You send a request with Idempotency-Key: <unique-value>
  2. RohoPay processes it and stores the response (status code + body) against the key for 24 hours — success and error responses alike
  3. If you send the same key again within 24 hours, RohoPay replays the stored response — no second charge, and no second provider call
  4. If you send a new key, a new transaction is created

Generating Keys

Use a globally unique string that is derived from your own business record, not a fresh random value on every call — e.g. collect:<depositId> or disburse:<withdrawalId>. Create the record first, then build the key from its id.

⚠️

A random randomUUID() on every call gives no protection against your own retries: if a request times out and your code retries, the new random key creates a second payment. Use a random UUID only if you generate it once, store it with the record, and reuse it for every attempt of that record.

TypeScriptTypeScript
import { randomUUID } from "crypto";

const idempotencyKey = randomUUID(); // e.g., "a1b2c3d4-..."
PythonPython
import uuid
idempotency_key = str(uuid.uuid4())
GoGo
import "github.com/google/uuid"
key := uuid.New().String()
PHPPHP
$key = \Ramsey\Uuid\Uuid::uuid4()->toString();

Request Example

terminal.shBash
curl -X POST https://api.rohopay.com/api/v1/collect \
  -H "Authorization: Bearer live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{"phone": "256700123456", "amount": 50000, "currency": "UGX"}'

Idempotency Key Best Practices

PracticeRecommendation
ScopeOne key per business event (order, subscription charge, etc.)
StorageStore the key in your database alongside the order
FormatDerived from your record id (collect:<id>), or a UUID stored with the record
Replay windowThe same key returns the original response for 24 hours
New keyGenerate a new key for every intentionally new attempt

Which key when you retry

What happenedWhat to do
No response received (timeout, dropped connection)Retry with the same key. You get the original result and never a duplicate
A response arrived, any status (200, 4xx, 5xx)That response is now cached for the key. Retrying with the same key just replays it. To try again, start a new attempt: a new record and a new key
200 with status: "failed"The payment definitively failed. A new attempt needs a new key
⚠️

Error responses (including 5xx) are cached on purpose: it guarantees a client that retries after a failure can never reach the provider twice and double-charge. The flip side is that a retry with the same key will not re-run a request that previously errored.

Missing Idempotency Key

For collect, disburse, and checkout you must supply the key yourself. If you omit Idempotency-Key on one of these endpoints, you receive:

response.jsonJSON
{
  "error": "MISSING_IDEMPOTENCY_KEY",
  "message": "Idempotency-Key header is required for this endpoint"
}
💡

This particular error is a flat { "error", "message" } object (HTTP 400), not the usual { "success": false, "error": { … } } envelope. Handle both shapes.