Idempotency
Use idempotency keys to avoid duplicate charges and ensure an operation is executed only once.
Idempotency guarantees that the same operation, repeated multiple times, produces only one effect. In the context of payments, this is essential to avoid charging a customer twice when an API call is retried after a network error.
Mechanics vs. practice
This page brings together concrete strategies. For the reference mechanics (Idempotency-Key
header, 422 conflict on a mismatched payload, retention duration), see
API Reference → Idempotency.
Idempotency via the Idempotency-Key header
`reference` is NOT an idempotency key
The reference field you send when creating a payment has no uniqueness constraint —
sending the same reference twice creates two separate payments. The only real
protection against duplicate charges on retry is the Idempotency-Key header,
supported on POST /payments and POST /transfers. Always generate one and send it on
every write call you might retry.
curl https://api.wajub.com/payments \
-H "Authorization: sk_test.xxxx" \
-H "Idempotency-Key: idem-ORDER-4172-1716912000" \
-d '{
"amount": 5000,
"currency": "XAF",
"email": "buyer@example.com"
}'If the same Idempotency-Key is sent again with the same payload within 24h, the
original response is returned unchanged — no new payment is created. On POST /payments,
reusing the same key with a different payload returns a 422 explaining the conflict
(this payload check is not yet implemented on POST /transfers — treat keys as strictly
one-shot there too).
Key generation strategies
Approach 1: Order ID + unique suffix
// Generate a unique idempotency key on each retry attempt
const idempotencyKey = `ORDER-${orderId}-${Date.now()}`;
// Example: ORDER-4172-1716912000000This approach allows retrying a payment for the same order if the first attempt fails, while still deduplicating true network-retry duplicates.
Approach 2: Order ID only (strong lock)
// One order = one possible payment attempt per key
const idempotencyKey = `ORDER-${orderId}`;
// To allow a genuinely new attempt after a real failure:
const idempotencyKey = `ORDER-${orderId}-ATTEMPT-${attemptNumber}`;Approach 3: UUID
import { v4 as uuid } from 'uuid';
const idempotencyKey = uuid();Store the key in your database
Associate the idempotency key with your order in your own database. When in doubt,
retrieve the payment (GET /payments/{id}) and check its status rather than
reinitializing with a new key.
Webhook idempotency
Webhooks can be delivered multiple times (retries). Ensure your processing is idempotent by using the delivery's event identifier after verifying the signature (see Signature Verification):
const processedEvents = new Set(); // In production: Redis or a DB table
app.post('/webhooks/wajub', (req, res) => {
const event = verifyAndParse(req); // your own signature-verification helper
res.sendStatus(200);
if (processedEvents.has(event.id)) {
console.log('Event already processed, ignored:', event.id);
return;
}
processedEvents.add(event.id);
// Business processing...
});Transfers
The same Idempotency-Key mechanism applies to POST /transfers. The beneficiary is
either an existing beneficiary's id, or an inline object:
curl https://api.wajub.com/transfers \
-H "Authorization: sk_test.xxxx" \
-H "Idempotency-Key: idem-ORDER-4172-payout-1716912000" \
-d '{
"amount": 15000,
"currency": "XAF",
"beneficiary": { "name": "Jean Dupont", "channel": "cm.mtn", "phone": "+237670000001" },
"description": "Order #4172 payout"
}'If you send the same Idempotency-Key in a new call, the API returns the already
created transfer instead of creating a second one.
Good to know
Idempotency keys are cached for 24 hours (fast-path). Beyond that, a durable database uniqueness constraint on the key still prevents an accidental duplicate, but the fast, full-response replay only applies within the 24h window.