Ir al contenido

Idempotencia

Una operación es idempotente cuando ejecutarla una o más veces produce el mismo resultado. Es la propiedad crítica para que tu integración sea segura ante reintentos por timeout, caídas de red o retrys automáticos.

En Pickwise, toda operación de escritura (crear producto, crear orden, cancelar orden) es segura de reintentar gracias a dos mecanismos complementarios.

Los endpoints POST /products y POST /orders son upsert por externalId: si el externalId ya existe, el recurso se actualiza; si no existe, se crea.

Esto significa que, por diseño, podés llamar al mismo endpoint N veces con el mismo externalId y siempre quedás con el mismo recurso final. No hace falta hacer nada especial.

2. X-Idempotency-Key (opcional, para retries explícitos)

Sección titulada «2. X-Idempotency-Key (opcional, para retries explícitos)»

Cuando hacés un reintento porque no estás seguro si la request anterior llegó (típicamente: timeout de red), enviar la misma idempotency key garantiza que:

  • La segunda request devuelve la misma respuesta que la primera.
  • No se reprocesa el write.
  • Si los parámetros cambian entre una y otra, recibís 409 IDEMPOTENCY_CONFLICT.

Se habilita mandando el header:

X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

El valor debe ser un UUID v4 único por “intento lógico” de operación.

  1. Enviás una request con X-Idempotency-Key: <UUID>.
  2. Pickwise procesa la operación y cachea la respuesta asociada a esa key durante 24 horas.
  3. Si volvés a enviar una request con la misma key (y los mismos parámetros), Pickwise devuelve la respuesta cacheada sin reprocesar.
  4. La respuesta replayed incluye el header X-Idempotency-Replayed: true.
  • Siempre en operaciones críticas que no querés duplicar (crear órdenes, upsert de productos).
  • Cuando hay riesgo de reenvío: timeout de red, cola de mensajes con retry automático, cron job que puede superponerse.
  • Junto con backoff exponencial para reintentos ante 5xx.
Ventana de terminal
# Primera request
curl -X POST https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/products \
-H "Authorization: Bearer pk_live_xxxx" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{"externalId":"P1","sku":"SKU1","name":"Prod 1","stock":10}'
# -> 201 Created
# Reintento por timeout — misma key, mismo body
curl -X POST https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/products \
-H "Authorization: Bearer pk_live_xxxx" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{"externalId":"P1","sku":"SKU1","name":"Prod 1","stock":10}'
# -> 201 Created (cacheada), X-Idempotency-Replayed: true
EscenarioResultado
Primera request con key XSe procesa y se cachea la respuesta por 24h.
Segunda request con key X, mismos parámetrosDevuelve la respuesta cacheada. Header X-Idempotency-Replayed: true. No reprocesa.
Segunda request con key X, parámetros distintos409 IDEMPOTENCY_CONFLICT. La key ya está asociada a una request diferente.
Request con key X después de 24hSe procesa como una operación nueva.

Un cliente Node.js que combina timeout, retry con backoff y idempotency key:

import { randomUUID } from 'crypto';
async function createOrderWithRetry(orderPayload, maxRetries = 3) {
const idempotencyKey = randomUUID();
let attempt = 0;
while (attempt < maxRetries) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
const res = await fetch(
'https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/orders',
{
method: 'POST',
signal: controller.signal,
headers: {
'Authorization': `Bearer ${process.env.PICKWISE_API_KEY}`,
'Content-Type': 'application/json',
'X-Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderPayload)
}
);
clearTimeout(timeout);
// 4xx: no reintentar (excepto 429).
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
return res;
}
// 2xx: éxito (incluyendo response replayed).
if (res.ok) {
return res;
}
// 429 / 5xx: reintentar.
attempt++;
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
} catch (err) {
attempt++;
if (attempt >= maxRetries) throw err;
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
throw new Error('Max retries exceeded');
}

La clave: la misma idempotencyKey se usa en los 3 intentos. Si el primero llegó pero se perdió la respuesta por timeout, el segundo devuelve la respuesta cacheada y no duplica la orden.