> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cleo-pay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Send an Idempotency-Key with every write so a retry returns the first answer instead of running again. Money writes require one and never run twice under one key.

Networks fail. An `Idempotency-Key` lets you resend a write without doing it twice.

```http theme={null}
Idempotency-Key: 3b0f6d4e-7a51-4c2b-9d8e-1f2a3b4c5d6e
```

* 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

| Writes | Key |
| - | - |
| `POST /v1/payments`, `POST /v1/payments/{paymentId}/cancel`, `POST /v1/payables/{payableId}/retry-payment`, `POST /v1/balance/deposits`, `POST /v1/balance/withdrawals` | **Required.** Without one, the call answers `400 validation_error`. |
| `POST /v1/payables`, `POST /v1/payables/{payableId}/submit`, contact create, update, archive and unarchive, invoice create, send, void, mark-paid and payment-link, webhook endpoint update and delete, delivery redeliver, and the deprecated bank routes | Optional. Send one whenever a timeout could make you retry. |
| `POST /v1/webhook-endpoints`, `POST /v1/webhook-endpoints/{id}/rotate-secret` | Not accepted. A replay would have to store and return the secret again. |

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.

| Situation | Answer |
| - | - |
| Same key, same request, first request finished | The first answer, replayed. The action does not run again. |
| Same key, different body, path or MCP tool | `409 idempotency_conflict`. Nothing is replayed. |
| Same key while the first request is still running | `409 idempotency_conflict`. Wait, then retry with the same key. |
| Same key after the first request died without an answer | `409 idempotency_conflict` for up to 5 minutes. After that, the next retry runs the action. Money writes are the exception, below. |

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](/guides/mcp-server#retrying-safely).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.