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

# Retries and idempotency

> Idempotency keys prevent duplicate operations on Midaz transaction endpoints when retrying transient failures.

A key aspect of Midaz’s reliability is making sure operations are **safe to retry** and never processed more than once.

Whether you're handling transactions or creating entities, your integration should be resilient, even when facing network hiccups, timeouts, or temporary disruptions. This page explains how Midaz protects against duplication using **idempotency keys**, and how you can implement them to handle retries in your system confidently.

### Making retries safe

In real-world systems, failed API requests are common. Maybe a network timeout occurs. Maybe your service goes down right after sending a request. In these cases, it’s natural to retry, but how can you be sure Midaz won’t process the same operation twice?

That’s where **idempotency keys** come in.

By attaching a unique key to each request, you’re telling Midaz: "This is the same operation. If you’ve already processed it, don’t do it again."

Midaz stores this key temporarily and uses it to determine whether the request is new, already completed, or still being processed. This protects your system from duplicates while giving you full control over your retry strategy.

## Idempotency in Midaz

***

Midaz uses **idempotency keys** to make sure transaction operations are safe to retry and never processed more than once. This mechanism is available on all **transaction endpoints**: `/transactions/json`, `/transactions/dsl`, `/transactions/inflow`, `/transactions/outflow`, `/transactions/annotation`, and `/transactions/{id}/revert`.

<Note>
  Other Lerian products also support idempotency through their own headers — see the [Idempotency across Lerian products](#idempotency-across-lerian-products) section below. This page focuses on idempotency for the Midaz Ledger API.
</Note>

<Note>
  The `commit` and `cancel` transaction endpoints use a Redis-based lock to prevent concurrent processing of the same transaction, but they do not support full idempotency (no cached responses or `X-Idempotency-Replayed` header).
</Note>

To use it, your request can include two headers:

* `X-Idempotency`: the unique key that identifies the request.
* `X-TTL`: the time-to-live (in seconds) that Midaz should store this key in cache.

<Note>
  If you don’t send the `X-Idempotency` header, Midaz automatically generates one by computing a SHA-256 hash of the request body. This means identical request bodies sent to the same organization and ledger are automatically deduplicated.
</Note>

Here’s what happens behind the scenes:

1. When a new key arrives, Midaz marks it as `pending`, processes the request, and stores the full response in cache.
2. If the same key is used again within the TTL window:
   1. If the operation is still running, Midaz returns a `409 Conflict` (error code `0084`) with `X-Idempotency-Replayed: false`.
   2. If it’s done, Midaz returns the exact same response with a `201 Created` status code and `X-Idempotency-Replayed: true`.
3. If the transaction fails due to validation or insufficient balance errors, Midaz **deletes the idempotency key**, allowing you to retry with the same key after fixing the issue.

In Midaz, idempotency keys are scoped by **organization and ledger**. This means the same key value can be used independently across different organizations or ledgers without conflict. Other Lerian products use different scoping strategies — see the [comparison table](#idempotency-across-lerian-products) for details.

### Workflow summary

*Figure 1* shows the full lifecycle of an idempotent request:

<Frame caption="Figure 1. Idempotent request lifecycle in Midaz.">
  <img src="https://mintcdn.com/lerian-49cb71fc/YxwrLF5ibN6V-UcN/images/en/reference/idempotency.jpg?fit=max&auto=format&n=YxwrLF5ibN6V-UcN&q=85&s=a9b9750be23fc0031c0f1a9b50cc8bc1" alt="" width="1399" height="1116" data-path="images/en/reference/idempotency.jpg" />
</Frame>

**How it works:**

* If the request **doesn’t include an existing idempotency key**, Midaz creates a new one (or auto-generates one from the request body hash), processes the request, stores the response, and returns it with `X-Idempotency-Replayed: false`.
* If the key **already exists**:
  * If the operation is still running, Midaz returns a `409 Conflict` with `X-Idempotency-Replayed: false`.
  * If the operation is complete, Midaz skips execution and returns the cached response with a `201 Created` status code and `X-Idempotency-Replayed: true`.
* If the original request **failed due to validation or balance errors**, the key is cleaned up automatically, so you can safely retry with the same key.

This behavior ensures every request is handled safely, predictably, and without duplication, even when retried.

### Example request

Here’s how to send an idempotent request to create a transaction:

<CodeGroup>
  ```http HTTP theme={null}
  POST /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/json HTTP/1.1
  X-Idempotency: 7fb8e1d098cd4730bb932d038b3b8651
  X-TTL: 60
  Content-Type: application/json

  {
    "description": "Monthly subscription payment",
    "code": "SUB-2025-001",
    "send": {
      "asset": "USD",
      "value": "1500",
      "source": {
        "from": [
          {
            "accountAlias": "customer-usd-1",
            "amount": {
              "asset": "USD",
              "value": "1500"
            }
          }
        ]
      },
      "distribute": {
        "to": [
          {
            "accountAlias": "merchant-usd-1",
            "amount": {
              "asset": "USD",
              "value": "1500"
            }
          }
        ]
      }
    }
  }
  ```
</CodeGroup>

If the request succeeds and you send it again within 60 seconds, Midaz will return the cached result with:

<CodeGroup>
  ```http HTTP theme={null}
  HTTP/1.1 201 Created
  X-Idempotency-Replayed: true
  ```
</CodeGroup>

If you send it again while the original is still being processed, you’ll receive:

<CodeGroup>
  ```http HTTP theme={null}
  HTTP/1.1 409 Conflict
  X-Idempotency-Replayed: false
  ```
</CodeGroup>

## Key generation

***

You can provide your own `X-Idempotency` key or let Midaz generate one automatically.

### Automatic key generation

If you omit the `X-Idempotency` header, Midaz computes a SHA-256 hash of the request body and uses it as the idempotency key. This means that sending the exact same JSON body to the same organization and ledger will be automatically deduplicated — no extra work needed.

This is sufficient for most retry scenarios where the request body doesn’t change between attempts.

### Custom key generation

Use a custom key when you need to:

* Correlate the idempotency key with an ID in your own system (e.g., an order ID).
* Retry with a modified request body while still deduplicating (e.g., after correcting a field).
* Control the key format for logging or auditing purposes.

The key should be deterministic: if the same logical operation is retried, the key remains the same. A common approach is to use a UUID or a hash based on your internal reference:

<CodeGroup>
  ```python Python theme={null}
  import uuid

  # Option 1: Use an existing business ID as the key
  idempotency_key = f"order-{order_id}"

  # Option 2: Generate a UUID and store it for retries
  idempotency_key = str(uuid.uuid4())
  ```
</CodeGroup>

## Best practices

***

### Always validate the `X-Idempotency-Replayed` header

When your system receives a response from a transaction endpoint, **always check the `X-Idempotency-Replayed` response header** before processing the result. This header tells you whether the response is from a new operation or a cached replay:

* `X-Idempotency-Replayed: false` — This is a fresh response. The transaction was just processed.
* `X-Idempotency-Replayed: true` — This is a cached response. The transaction was already processed previously.

<Warning>
  Failing to check this header is a common integration mistake. Without it, your system may interpret a replayed response as a new transaction, leading to duplicate processing on your side — even though Midaz only executed it once.
</Warning>

For example, if your system settles boletos (bank slips) based on transaction responses, you must verify `X-Idempotency-Replayed` to avoid settling the same boleto twice.

### Use explicit idempotency keys for critical flows

While Midaz auto-generates keys from the request body, for critical financial flows (settlements, payouts, transfers), always provide an explicit `X-Idempotency` key tied to your business process ID. This gives you:

* Full control over deduplication, even if the request body changes slightly between retries.
* A clear audit trail linking Midaz transactions to your internal operations.
* Protection against edge cases where request serialization might differ.

### Set appropriate TTL values

Choose TTL values that match your retry window:

* For synchronous operations with fast retries: **60–120 seconds**.
* For asynchronous workflows with potential delays: **300–600 seconds**.
* For batch processing with long retry windows: consider longer TTLs and explicit keys.

## Retry strategy

***

When a request fails, how you retry matters. Here are recommended patterns for handling different failure scenarios:

### Retryable failures

These failures are safe to retry with the same idempotency key:

| Scenario                                            | What to do                                                                                                         |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Network timeout or connection error                 | Retry with the same key and body. If the original request was processed, you'll get the cached response.           |
| `5xx` server error                                  | Retry with exponential backoff. The server may be temporarily overloaded.                                          |
| `409 Conflict` with `X-Idempotency-Replayed: false` | The previous request is still being processed. Wait and retry after a short delay.                                 |
| Validation or balance error                         | Fix the issue in your request, then retry with the same idempotency key (Midaz deletes the key on these failures). |

### Non-retryable failures

These failures require a different approach:

| Scenario                             | What to do                                                            |
| ------------------------------------ | --------------------------------------------------------------------- |
| `400 Bad Request` (schema error)     | Fix the request format. Do not retry the same payload.                |
| `401 Unauthorized` / `403 Forbidden` | Check your authentication credentials. Retrying won't help.           |
| `404 Not Found`                      | Verify the organization, ledger, or account IDs in your request path. |

### Exponential backoff

For transient errors, use exponential backoff with jitter to avoid overwhelming the server:

<CodeGroup>
  ```python Python theme={null}
  import time
  import random
  import requests

  def create_transaction_with_retry(idempotency_key, payload, max_retries=5, base_delay=1.0):
      """Retries with the same idempotency key on every attempt."""
      for attempt in range(max_retries):
          try:
              response = requests.post(
                  f"{BASE_URL}/v1/organizations/{org_id}/ledgers/{ledger_id}/transactions/json",
                  headers={
                      "X-Idempotency": idempotency_key,
                      "X-TTL": "300",
                      "Content-Type": "application/json",
                  },
                  json=payload,
              )
              response.raise_for_status()
              return response.json()
          except requests.exceptions.RequestException:
              if attempt == max_retries - 1:
                  raise
              delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
              time.sleep(delay)
  ```
</CodeGroup>

A typical retry sequence with this pattern would be: 1s, 2s, 4s, 8s, 16s (plus random jitter on each attempt).

## Preventing entity duplication

***

For some endpoints, you don’t need idempotency keys to avoid duplication. Midaz enforces **uniqueness constraints** on critical resources. If you attempt to create an entity that conflicts with an existing one, the system blocks the request and returns a `409 Conflict` with a descriptive error:

| Resource     | Unique field                               | Error code |
| ------------ | ------------------------------------------ | ---------- |
| Ledger       | Name (within organization)                 | `0002`     |
| Asset        | Name or code (within ledger)               | `0003`     |
| Segment      | Name (within ledger)                       | `0015`     |
| Account      | Alias (within organization and ledger)     | `0020`     |
| Account Type | Key value (within organization and ledger) | `0108`     |

This ensures that your data remains clean, unambiguous, and easy to manage, even when multiple services are operating in parallel or when retries occur automatically. Unlike idempotency keys, these constraints are permanent and don’t expire.

## Idempotency across Lerian products

***

Multiple Lerian products support idempotency, each with its own header convention. Most products return the `X-Idempotency-Replayed` response header to indicate whether the response is a cached replay. Midaz always includes this header (`false` for new requests, `true` for replays), while other services only add it when the response is a replay — if the header is absent, the request was processed as new. PIX Direct does not return this header.

| Product          | Request header                                       | Accepts `X-TTL`    | `X-Idempotency-Replayed` | Scope                                   | Covered endpoints                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------- | ---------------------------------------------------- | ------------------ | ------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Midaz**        | `X-Idempotency`                                      | Yes (default 300s) | Always (`false`/`true`)  | Per organization and ledger             | Transaction creation, annotation, and revert (6 endpoints)                                                                                                                                                                                                                                                                                                                          |
| **Matcher**      | `X-Idempotency-Key` (also accepts `Idempotency-Key`) | No                 | On replay only           | Per tenant, method, and path            | All POST/PUT/PATCH endpoints (\~33 endpoints via global middleware). Callback endpoints use a failed-reacquire pattern: if a previous callback with the same key failed, Matcher automatically reacquires the lock and reprocesses the request. See [Webhooks and callbacks](/en/matcher/integrations/matcher-webhooks-callbacks#automatic-retry-for-failed-callbacks) for details. |
| **TED**          | `X-Idempotency-Key`                                  | No                 | On replay only           | Per idempotency key (no tenant scoping) | Message submit, return, and cancel (3 endpoints)                                                                                                                                                                                                                                                                                                                                    |
| **PIX Indirect** | `X-Idempotency`                                      | Yes                | On replay only           | Per account                             | Cashout initiate, cashout process, and refund (3 endpoints)                                                                                                                                                                                                                                                                                                                         |
| **PIX Direct**   | `Idempotency-Key`                                    | No                 | No                       | Per key                                 | Payment creation and return initiation (2 endpoints)                                                                                                                                                                                                                                                                                                                                |
| **Reporter**     | `X-Idempotency`                                      | No                 | On replay only           | Per key                                 | Report and template creation (2 endpoints)                                                                                                                                                                                                                                                                                                                                          |

<Note>
  Midaz always includes the `X-Idempotency-Replayed` header in the response (`false` for new requests, `true` for replays). Other services (Matcher, TED, PIX Indirect, and Reporter) only add this header when the response is a replay — if the header is absent, the request was processed as new. PIX Direct does not return this header; its idempotency middleware replays the full response transparently without a replay indicator.
</Note>

<Note>
  Fees Engine, Tracer, Auth, and CRM do not currently support idempotency headers. Tracer validations are stateless and naturally safe to retry. Auth token operations are inherently idempotent. CRM and onboarding entities rely on [uniqueness constraints](#preventing-entity-duplication) instead.
</Note>

## FAQ

***

<AccordionGroup>
  <Accordion title="What happens if I don’t send an idempotency key?">
    Midaz automatically generates one by computing a SHA-256 hash of the request body. This means identical request bodies sent to the same organization and ledger are automatically deduplicated. You only need to provide a custom key if you want to control deduplication independently from the request body.
  </Accordion>

  <Accordion title="Can I reuse a key across different organizations or ledgers?">
    Yes. Idempotency keys are scoped per organization and ledger, so the same key value used in different organizations or ledgers won’t conflict. However, within the same organization and ledger, each key must be unique per operation.
  </Accordion>

  <Accordion title="Can I reuse a key across different endpoints?">
    In Midaz, idempotency keys are scoped per organization and ledger, not per endpoint. If you reuse the same key on a different endpoint within the same organization and ledger, you will receive the cached response from the original endpoint. Always use unique keys for each distinct operation.
  </Accordion>

  <Accordion title="What happens if I change the TTL on a retry?">
    Only the TTL from the first request is used. Changing it later has no effect.
  </Accordion>

  <Accordion title="Will the replayed response always be identical?">
    Yes. Midaz replays the full response, including status code (`201 Created`), headers, and body, for completed requests.
  </Accordion>

  <Accordion title="What’s the default TTL if I don’t send X-TTL?">
    The default window is **300 seconds** (5 minutes).
  </Accordion>

  <Accordion title="What happens if the original request fails?">
    If the transaction fails due to validation errors or insufficient balance, Midaz deletes the idempotency key from cache. This allows you to fix the issue and retry with the same key.
  </Accordion>
</AccordionGroup>
