Mosce ERP · Centro de ayuda
Webhooks

Idempotencia (Idempotency-Key)

Cómo usar el encabezado Idempotency-Key para que los reintentos de tus integraciones no dupliquen órdenes de compra ni apliquen una recepción dos veces.

Si integras Mosce ERP con un sistema externo (un ERP de terceros, el servicio contable de tu negocio o tus propios scripts), el encabezado Idempotency-Key te garantiza que un reintento por timeout de red, un redespliegue o un doble envío no cree dos veces la misma operación. El servidor reconoce que ya procesó esa operación y devuelve la respuesta original.

Tiempo de lectura: ~6 min

¿Usas Mosce ERP solo desde la interfaz web? Entonces no tienes que hacer nada: la aplicación genera y envía la llave automáticamente. Este artículo es para integradores que llaman a la API directamente.

Qué es y por qué importa

Algunas operaciones de la API mutan datos (crean una orden de compra, la envían, la reciben, la cancelan). Si tu integración envía la petición y la red se corta antes de recibir la respuesta, no sabes si la operación se completó. Reintentar a ciegas podría crear una segunda orden o aplicar una recepción dos veces.

El encabezado Idempotency-Key resuelve esto: identifica un intento lógico del usuario. Si reintentas con la misma llave, el servidor te devuelve el resultado de la primera ejecución en vez de ejecutar de nuevo.

Endpoints que requieren el encabezado

Estos endpoints de mutación del módulo de compras requieren Idempotency-Key:

  • POST /api/v1/purchases - crear orden de compra.
  • POST /api/v1/purchases/{id}/send - enviar la orden al proveedor.
  • POST /api/v1/purchases/{id}/cancel - cancelar la orden.
  • POST /api/v1/purchases/{id}/receive - recibir mercancía.
  • POST /api/v1/purchases/{purchaseId}/receipts/{receiptId}/emit-e41 - emitir el comprobante de la recepción.

Período de gracia para integraciones existentes: si una integración aún no envía el encabezado, su próxima llamada a estos endpoints fallará con 400 IDEMPOTENCY_KEY_REQUIRED. Coordina con tu integrador para agregarlo antes de la próxima versión mayor.

Cómo generar la llave

  • Formato: una cadena URL-safe de 16 a 128 caracteres usando el alfabeto [A-Za-z0-9_-].
  • Recomendado: un UUID v4 (crypto.randomUUID(), 36 caracteres), un UUID v7 (ordenable), un ULID (26 caracteres) o un token base64url aleatorio. Todos son válidos.
  • Una llave por intento lógico. Genera una llave nueva por cada operación distinta. Reúsala SÓLO cuando reintentas exactamente la misma operación (por ejemplo, tras un timeout).
  • No uses valores predecibles ni cortos: menos de 16 caracteres del alfabeto URL-safe deja muy poca entropía y abre riesgo de colisión accidental entre operaciones concurrentes.

Las respuestas posibles

SituaciónRespuesta
Primera vez con esa llaveEl servidor ejecuta la operación y devuelve su resultado normal.
Reintento con la misma llave y el mismo cuerpoEl servidor devuelve la respuesta original (mismo código y cuerpo) sin ejecutar de nuevo. Esta es la garantía de "una sola ejecución".
Misma llave pero cuerpo diferente422 IDEMPOTENCY_KEY_BODY_MISMATCH - estás reciclando la llave para otra operación. Genera una llave nueva.
Dos reintentos concurrentes (misma llave, mismo cuerpo)Uno ejecuta; los demás esperan hasta 30 s a que termine y luego reciben la misma respuesta. Si el primero se cuelga más de 30 s, los demás reciben 409 IDEMPOTENCY_KEY_TIMED_OUT y pueden reintentar después.

Requisito crítico: el cuerpo debe ser byte-idéntico

El servidor calcula una huella del cuerpo JSON crudo y la compara contra la del primer envío. Por eso, en un reintento, el cuerpo debe serializarse byte por byte igual al original:

  • Mismo orden de propiedades (o usa una serialización canónica).
  • Mismo espaciado.
  • Mismas representaciones numéricas - 5 y "5" producen huellas distintas aunque el servidor luego los interprete igual.

Si no puedes garantizar bytes idénticos entre reintentos, entonces no es un reintento real: genera una llave nueva (es una operación nueva).

Códigos de error

CódigoSignificadoQué hacer
IDEMPOTENCY_KEY_REQUIRED (400)Falta el encabezado en un endpoint que lo exige.Agrega Idempotency-Key.
IDEMPOTENCY_KEY_INVALID_FORMAT (400)El valor está fuera del formato (16 - 128 caracteres URL-safe).Genera un valor válido (UUID/ULID).
IDEMPOTENCY_KEY_BODY_MISMATCH (422)Reusaste la llave con un cuerpo distinto.Usa una llave nueva por operación nueva.
IDEMPOTENCY_KEY_TIMED_OUT (409)Otro reintento concurrente tardó demasiado.Reintenta después; el sistema libera el bloqueo automáticamente.
IDEMPOTENCY_REQUIRES_TENANT (400)La petición no resolvió un tenant válido.Revisa tus credenciales/token.

Ventana de retención

La llave queda asociada a su resultado durante 24 horas desde el primer uso. Pasado ese tiempo se libera y podría reutilizarse - pero lo recomendado es generar siempre una llave nueva por operación.

Ejemplo con curl

curl -X POST https://api.tu-dominio.com/api/v1/purchases \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1f2c4a9e-7b3d-4e21-9a6c-8d5f0b2e1c34" \
  -d '{"supplierId":"sup_123","items":[{"productId":"prd_9","orderedQuantity":10,"unitCost":11000}]}'

Si la llamada falla por timeout, repite exactamente la misma petición (mismo Idempotency-Key y mismo cuerpo): obtendrás el resultado original sin crear una segunda orden.

Ejemplo con fetch (JavaScript)

const idempotencyKey = crypto.randomUUID();
const body = JSON.stringify({
  supplierId: 'sup_123',
  items: [{ productId: 'prd_9', orderedQuantity: 10, unitCost: 11000 }],
});

async function createPurchase() {
  return fetch('https://api.tu-dominio.com/api/v1/purchases', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey, // reusar el MISMO valor y el MISMO body al reintentar
    },
    body, // reusar la MISMA cadena exacta
  });
}

Relacionados