Idempotencia y reintentos
La idempotencia garantiza que ejecutar el mismo request varias veces produzca el mismo resultado que ejecutarlo una sola vez.
Cabecera X-Idempotency-Key obligatoria en los 6 POST que mueven dinero: /v1/co/payin/push, /v1/co/payin/qr, /v1/co/breb/payin/qr, /v1/co/breb/payin/key, /v1/co/payout/disburse y /v1/co/breb/payout. Sin ella, la petición se rechaza con 400 IDEMPOTENCY_KEY_REQUIRED antes de crear la transacción, retener saldo o llamar al proveedor.
Formato: hasta 128 caracteres de [A-Za-z0-9_-:.]. Fuera de ahí, 400 IDEMPOTENCY_KEY_INVALID.
Cómo usarla
Sección titulada «Cómo usarla»POST /v1/co/payout/disburseX-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000- El valor lo generas tú; se recomienda un UUID v4.
- La cabecera no forma parte del string firmado: no cambia tu firma ECDSA.
- Una clave por operación de negocio, no por request: el reintento de la misma operación la repite; una operación nueva estrena clave.
Comportamiento
Sección titulada «Comportamiento»| Situación | Respuesta |
|---|---|
| Cabecera ausente | 400 IDEMPOTENCY_KEY_REQUIRED. La petición no se procesa. |
| Clave con formato inválido | 400 IDEMPOTENCY_KEY_INVALID. |
| Clave nueva | Se procesa normal (201). |
| Clave ya usada por tu transacción, con el mismo cuerpo | 200 con la transacción original y "message": "Transaction already exists (idempotent)". |
| Clave ya usada con un cuerpo distinto | 422 IDEMPOTENCY_KEY_REUSED. La petición no se procesa. |
| Otra petición con esa clave está en vuelo | 409 IDEMPOTENCY_IN_FLIGHT: reintentar en unos segundos con la misma clave. |
| Clave tomada y sin transacción que devolver | 409 IDEMPOTENCY_CONFLICT. |
El espacio de claves es por aliado: puedes usar tus propios identificadores de pedido sin miedo a chocar con otro comercio. La comparación de cuerpo es sobre el JSON canonicalizado, así que reordenar los campos en un reintento no cuenta como cuerpo distinto — pero cambiar el monto o el destinatario, sí: eso es reciclar la clave y se rechaza en vez de devolverte la operación vieja como si fuera la nueva.
La reserva es atómica. Si el proveedor rechaza la operación (4xx claro) la clave se libera y puedes reintentar con la misma; ante un timeout o 5xx no se libera, porque la operación pudo haberse ejecutado. Las claves caducan a los 90 días.
Cuándo usarla
Sección titulada «Cuándo usarla»- Genera la clave antes del primer intento y reutilízala en todos los reintentos tras un error de red o un timeout.
- Cuando el resultado del request original es incierto (no sabes si llegó al servidor).
- No la reutilices en operaciones nuevas: sería un
422(cuerpo distinto) o te devolvería la transacción vieja.
Ejemplo con reintentos
Sección titulada «Ejemplo con reintentos»async function disburseWithRetry(phoneNumber, amount, reference) { // Generar la clave ANTES del loop — la misma en todos los reintentos const idempotencyKey = crypto.randomUUID(); const path = '/v1/co/payout/disburse'; const bodyString = JSON.stringify({ phoneNumber, amount, reference });
for (let attempt = 1; attempt <= 3; attempt++) { const response = await fetch(`${BASE_URL}${path}`, { method: 'POST', headers: { ...buildAuthHeaders('POST', path, bodyString), 'X-Idempotency-Key': idempotencyKey, // ← siempre la misma }, body: bodyString, });
// 201 = creada; 200 = respuesta idempotente de un intento anterior if (response.ok) return response.json();
// 5xx: la dispersión pudo haber salido. Reintentar con la MISMA clave. if (response.status >= 500) { await sleep(Math.pow(2, attempt) * 1000); // backoff exponencial continue; }
throw new Error(`Error ${response.status}`); }
// Se agotaron los reintentos: consultar estado, nunca crear otra dispersión throw new Error('Dispersión sin confirmar — consultar por externalId');}