MoniWave

Idempotency

Every mutating request (POST) requires an Idempotency-Key header containing a UUID you generate:

Idempotency-Key: 3f9c1e2a-7b4d-4a1f-9c8e-2d6b5a0f3e11

This is not optional and not advisory. Network timeouts happen; without an idempotency key, a retry after a timeout can charge a customer twice. With one, retrying is always safe.

How it behaves

SituationResult
First request with a keyExecutes normally.
Same key, same bodyReturns the original response, byte-for-byte, with the original status code and the header Idempotency-Replayed: true. No second transaction is created.
Same key, different body409 idempotency_key_reuse. Keys are not reusable for different requests.
Same key while the first is still running409 idempotency_in_progress. Retry shortly.
First request failed (5xx or crash)The key is released, so retrying re-executes. You never get stuck replaying a failure.

Two requests with the same key arriving simultaneously are safe: exactly one executes, and the other replays or gets idempotency_in_progress.

Choosing keys

  • One key per logical operation, generated by you, ideally tied to something in your system: an order id, a payout batch line, a retry group. uuidgen, crypto.randomUUID(), or Guid.NewGuid() all work.
  • Reuse the same key when retrying the same operation — that is the entire point.
  • Never reuse a key across different operations. Two different charges need two different keys.
  • Keys are scoped per merchant, so they cannot collide with another merchant's.

Retry pattern

const idempotencyKey = crypto.randomUUID();   // generated ONCE, outside the retry loop

async function createCollection() {
  for (let attempt = 1; attempt <= 3; attempt++) {
    const response = await fetch('https://api-dev.moni-wave.com/v1/collections', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${SECRET_KEY}`,
        'Idempotency-Key': idempotencyKey,      // same key on every attempt
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ amount: 5000, currency: 'XAF', msisdn: '237690000001', description: 'Order #1042' }),
    });

    if (response.status === 409) {
      const { error } = await response.json();
      if (error.code === 'idempotency_in_progress') {
        await sleep(1000 * attempt);
        continue;                                // the original is still running
      }
      throw new Error(error.code);               // idempotency_key_reuse: a bug in your code
    }

    return response.json();                      // 201 fresh, or replayed original
  }
}

The key is generated outside the loop. Generating it inside would create a new transaction per attempt — exactly the bug idempotency exists to prevent.

Errors

HTTPCodeMeaning
400idempotency_key_requiredHeader missing on a mutating request.
400idempotency_key_invalidHeader present but not a UUID.
409idempotency_key_reuseKey already used with a different request body.
409idempotency_in_progressThe original request with this key is still executing.

Relationship to webhooks

Idempotency protects requests you send us. Webhooks are the mirror image: they are delivered at-least-once, so your endpoint must tolerate duplicates. Each webhook carries a stable event id for exactly that purpose — see webhooks.md.