Ir al contenido

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.

POST /v1/co/payout/disburse
X-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.
SituaciónRespuesta
Cabecera ausente400 IDEMPOTENCY_KEY_REQUIRED. La petición no se procesa.
Clave con formato inválido400 IDEMPOTENCY_KEY_INVALID.
Clave nuevaSe procesa normal (201).
Clave ya usada por tu transacción, con el mismo cuerpo200 con la transacción original y "message": "Transaction already exists (idempotent)".
Clave ya usada con un cuerpo distinto422 IDEMPOTENCY_KEY_REUSED. La petición no se procesa.
Otra petición con esa clave está en vuelo409 IDEMPOTENCY_IN_FLIGHT: reintentar en unos segundos con la misma clave.
Clave tomada y sin transacción que devolver409 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.

  • 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.
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');
}