Skip to main content
Networks fail. An Idempotency-Key lets you resend a write without doing it twice.
  • Use a new key, such as a UUID, for each new action.
  • Use the same key, with the same body, when you resend that action.
  • Keys are up to 64 characters. A longer key answers 400 validation_error.
  • Keys belong to the business. A key used through REST and the same key used through MCP are different requests and conflict.

Which writes take a key

Without a key, a retried POST /v1/payables, POST /v1/contacts or POST /v1/bank-accounts creates a second record. The contact duplicate guard usually catches a retried contact, but it does not replace a key.

What a retry gets

Cleo stores a fingerprint of the first request: its method, path and body. Key order and fields left out do not change the fingerprint. Which first answers are stored and replayed:
  • Successes are stored.
  • 4xx answers are stored, except 409, 429 and the state refusals below. A retry of a stored 400 gets the same 400.
  • State refusals are not stored: payment_in_progress, insufficient_balance, setup_required, approval_required, payments_locked, setup_incomplete, feature_unavailable and contact_not_payable. A retry with the same key checks again, so it can succeed once the state changes.
  • 409 and 429 answers are not stored. A retry runs the action again.
  • 5xx answers are not stored, except for money writes.

Money writes never run twice

Payments, cancels, retries, deposits and withdrawals are money writes. Once one has started:
  • A failure that may have moved money (any 5xx, including payment_unconfirmed) is final for that key. Every retry with the key answers 409 payment_unconfirmed.
  • A first request that died without an answer, and is more than 5 minutes old, also answers 409 payment_unconfirmed.
  • Refusals raised before anything is sent (the state refusals above) still let a same-key retry try again.
When you get payment_unconfirmed, the money may still move. Look for the payment with GET /v1/payments, or the transfer with GET /v1/balance/transactions. Send it again with a new key only once you know the first did not go out.

MCP

MCP write tools take the same key as an idempotencyKey argument. It is bound to the tool and its arguments. See MCP server.