Idempotencia
¿Qué es idempotencia?
Sección titulada «¿Qué es 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.
Dos mecanismos de idempotencia
Sección titulada «Dos mecanismos de idempotencia»1. Idempotencia por externalId (nativa)
Sección titulada «1. Idempotencia por externalId (nativa)»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-446655440000El valor debe ser un UUID v4 único por “intento lógico” de operación.
Cómo funciona
Sección titulada «Cómo funciona»- Enviás una request con
X-Idempotency-Key: <UUID>. - Pickwise procesa la operación y cachea la respuesta asociada a esa key durante 24 horas.
- Si volvés a enviar una request con la misma key (y los mismos parámetros), Pickwise devuelve la respuesta cacheada sin reprocesar.
- La respuesta replayed incluye el header
X-Idempotency-Replayed: true.
Cuándo usarla
Sección titulada «Cuándo usarla»- 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.
Ejemplos
Sección titulada «Ejemplos»# Primera requestcurl -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 bodycurl -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: trueimport { randomUUID } from 'crypto';
const idempotencyKey = randomUUID();
async function createProduct(payload) { return fetch('https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/products', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PICKWISE_API_KEY}`, 'Content-Type': 'application/json', 'X-Idempotency-Key': idempotencyKey }, body: JSON.stringify(payload) });}
// Llamás la función N veces con la misma key en reintentos.// Pickwise procesa una sola vez.await createProduct({ externalId: 'P1', sku: 'SKU1', name: 'Prod 1', stock: 10 });<?phpfunction createProduct(array $payload, string $idempotencyKey): string { $ch = curl_init('https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/products'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . getenv('PICKWISE_API_KEY'), 'Content-Type: application/json', 'X-Idempotency-Key: ' . $idempotencyKey ]); $response = curl_exec($ch); curl_close($ch); return $response;}
// Generar UUID v4$key = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
createProduct([ 'externalId' => 'P1', 'sku' => 'SKU1', 'name' => 'Prod 1', 'stock' => 10], $key);Qué pasa en cada caso
Sección titulada «Qué pasa en cada caso»| Escenario | Resultado |
|---|---|
Primera request con key X | Se procesa y se cachea la respuesta por 24h. |
Segunda request con key X, mismos parámetros | Devuelve la respuesta cacheada. Header X-Idempotency-Replayed: true. No reprocesa. |
Segunda request con key X, parámetros distintos | 409 IDEMPOTENCY_CONFLICT. La key ya está asociada a una request diferente. |
Request con key X después de 24h | Se procesa como una operación nueva. |
Buenas prácticas
Sección titulada «Buenas prácticas»Ejemplo: retry con idempotencia + backoff
Sección titulada «Ejemplo: retry con idempotencia + backoff»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.