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.2a7c5e91b0d34f68" \
-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 + attempt number (recommended)
Generate the key once, persist it, reuse it on retry
The key must be the same on every retry of the same logical operation. Generate it once before the first attempt, store it alongside the order, and re-send it unchanged on retries. Using a time-based suffix that changes on every call defeats the purpose — you get no deduplication protection.
// Generate once, store in your DB alongside the order.
// On retry, read from the DB and reuse the same key.
const idempotencyKey = `ORDER-${orderId}-ATTEMPT-${attemptNumber}`;
// Example: ORDER-4172-ATTEMPT-1 (increment only when deliberately starting over)This approach allows a deliberate new payment attempt (e.g. after a genuine failure and user confirmation) while still deduplicating accidental network retries.
Approach 2: Order ID only (strict single-attempt lock)
// One order = one possible payment attempt ever.
// Simple and safe — the most common pattern.
const idempotencyKey = `ORDER-${orderId}`;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.2a7c5e91b0d34f68" \
-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.
Was this page helpful?