Skip to content

Idempotency

Every call that moves money carries a key you choose:

X-Idempotency-Key: order-2026-09-03-00417

Required on POST /payments and POST /payouts. 8 to 128 characters.

You sendMoria does
Same key, same bodyReplays the stored result. No second payment, no second transfer.
Same key, different bodyRefuses with 409. The key is already spoken for.
New keyA new operation, every time.

So: one fresh key per distinct thing you want to happen, reused only when repeating a request that is identical in every byte.

Derive it from something in your own system that is already unique — an order id, an invoice number — rather than a random value you would have to store to reuse.

What to do instead:

  1. Retry with the same key. If the first attempt landed, you get the stored result. If it never landed, it runs now.
  2. If you would rather ask than retry, poll GET /payments/{id} or GET /payouts/{id}.
  3. Either way, wait for a terminal state. pending means unfinished, not lost.

Moria records the key before doing anything, and writes the response against it when the work completes. That covers the awkward case in the middle: the payment committed but the response never reached you. On your retry we rebuild the answer from the row rather than creating a second payment.

It also means a 409 is informative rather than annoying. It says the key you sent is already attached to a different request — usually a bug where a key is being reused across orders, which is exactly the bug you want reported loudly rather than silently honoured.

Two things that look related and are not:

  • A retried webhook. That is at-least-once delivery, deduplicated on the event id, and it is a different mechanism — see Webhooks.
  • A duplicate customer payment. If a customer genuinely pays a virtual account twice, that is two payments and you will hear about both. Idempotency governs your instructions to us, not your customer’s behaviour.