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

# Idempotencia

> Evita duplicar payment intents, addresses o payouts al reintentar peticiones.

Los endpoints que **crean o modifican** recursos sensibles exigen el header **`Idempotency-Key`** con un identificador único generado por tu sistema.

Si la red falla y reintentas con la **misma clave** y el **mismo cuerpo**, ZoPay no ejecutará la operación dos veces: devolverá el resultado de la primera ejecución exitosa.

## Endpoints que requieren `Idempotency-Key`

| Método | Ruta                        |
| ------ | --------------------------- |
| `POST` | `/payment-intents`          |
| `POST` | `/addresses`                |
| `POST` | `/payouts/quote`            |
| `POST` | `/payouts`                  |
| `POST` | `/events/{event_id}/replay` |

## Recomendación

Usa un **UUID v4** por intento de operación nuevo. No reutilices el mismo UUID entre operaciones distintas.

```bash theme={null}
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
```

## Ejemplo (crear payment intent)

```bash theme={null}
curl -X POST "https://api.zopay.cash/connect/v1/payment-intents" \
  -H "Authorization: Bearer sk_live_xxxxxxxx" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "25.00",
    "currencies": ["USDT"],
    "external_ref": "user_alice"
  }'
```

Reintentar la misma petición con la **misma** clave y **mismo** cuerpo devuelve el mismo `id` de intent sin crear uno nuevo.

## Reglas

* **Mismo cuerpo:** misma clave con cuerpo distinto → `409 idempotency_conflict`.
* **TTL:** las claves expiran tras un periodo limitado (típicamente 24 h). Pasado ese tiempo no puedes reintentar con la misma clave.
* **Replays silenciosos en sandbox:** [`POST /test/payment-intents/{id}/simulate-deposit`](/api-reference/endpoints/post-test-payment-intents-by-id-simulate-deposit) no requiere `Idempotency-Key` — su anchor es el `completed_at` del intent. Llamarlo dos veces es un no-op.

## Errores de validación — 422 vs 400

El OpenAPI documenta dos códigos distintos para errores tipo "bad request", con significados diferentes. Wirea el manejo de error de tu cliente alrededor de la distinción:

* **`422 Unprocessable Entity`** — la petición fue **estructuralmente inválida**. Falló la validación del body schema (Pydantic): un campo requerido faltaba, un campo tenía el tipo equivocado, se mandó un campo desconocido. El campo `error.param` nombra la llave ofensora. Arregla la forma del body y reintenta.
* **`400 Bad Request`** — la petición fue **bien-formada pero falló un check de negocio / runtime**. El body parseó limpio pero algo más lo rechazó: un valor fuera de rango, un conflicto de transición de estado, un asset que no existe en tu catálogo habilitado. Lee `error.code` y `error.message` para recuperarte.

Coincide con la convención moderna de Stripe: 400 para "HTTP malo", 422 para "HTTP bueno, payload semánticamente inválido". Tu cliente generado debería ramificar primero por el status code; los dos shapes cargan el mismo envelope `error`.

Guía paso a paso en [Primer payout](/quickstarts/first-payout) y [Checkout web (SDK)](/quickstarts/checkout-sdk).
