Integración ERP-PIM con Node.js: patrones, errores frecuentes y validaciones antes de salir a producción

Un recorrido técnico completo: manejo de identificadores, sincronización bidireccional y estrategias de recuperación ante errores en integraciones ERP-PIM.

Una integración ERP-PIM con Node.js está realmente preparada para producción cuando puede mantener la identidad de cada producto, respetar qué sistema gobierna cada dato, procesar cambios de forma incremental, reintentar operaciones sin generar duplicados y recuperarse de un fallo sin perder el estado de la sincronización.

Conseguir un 200 o un 201 en una prueba demuestra que una petición funcionó una vez. No demuestra que el integrador pueda procesar miles de productos, sobrevivir a un timeout, respetar los límites de una API, evitar duplicados después de un retry o retomar un proceso sin comenzar otra vez desde cero.

Ese es el salto importante entre conectar dos sistemas y construir una integración preparada para producción.

Una integración no está lista para producción porque un endpoint responde correctamente. Está lista cuando puede fallar, saber qué ocurrió y recuperarse sin comprometer los datos.

En otro artículo expliqué por qué ERP y PIM no hablan exactamente el mismo idioma y por qué el verdadero problema suele comenzar en el modelo de datos, no en la API. Antes de integrar hay que definir quién gobierna cada información, cómo se identifican los productos y qué transformaciones necesita cada sistema.

En este punto voy a asumir que ese contrato conceptual ya existe. Sabemos qué entra, qué sale y quién manda sobre cada atributo. La pregunta pasa a ser otra: ¿cómo construimos con Node.js un integrador ERP-PIM que pueda operar de manera confiable cuando aparecen volumen, errores y situaciones inesperadas?

Identidad del producto: SKU, IDs técnicos y claves externas

Cuando desarrollo una integración, uno de los primeros temas que intento cerrar es el manejo de identificadores.

A primera vista parece sencillo: el SKU identifica al producto. Pero después aparece un ERP con su propio código interno, el PIM asigna otro ID, el artículo tiene un GTIN, el eCommerce devuelve un identificador adicional y, además, existen relaciones entre productos padre y variantes que deben mantenerse.

En ese momento, el SKU deja de ser “el ID” y pasa a ser uno de los identificadores que existen dentro del ecosistema.

El problema se vuelve todavía más delicado cuando ese SKU puede modificarse. Akeneo, por ejemplo, permite trabajar con UUID como referencia técnica estable para un producto, independientemente de otros identificadores que puedan cambiar.

Esto no significa que cada integrador deba inventar sus propios UUID. Significa que conviene separar claramente tres conceptos: la clave técnica estable, el identificador de negocio y los IDs externos asignados por cada sistema conectado.

En muchos proyectos resulta útil persistir explícitamente esa correspondencia:

{
  erpId: "MAT-10452",
  sku: "REM-NEG-M",
  pimId: "7f0c...",
  parentId: "REM-NEG",
  lastSyncAt: "2026-08-27T18:00:00Z"
}

Una capa de identidad como esta evita terminar haciendo búsquedas ambiguas cada vez que necesitamos actualizar un producto.

Las variantes requieren todavía más cuidado. Una variante suele mantener su propio SKU o GTIN, pero comparte determinados atributos con un producto padre. Si ERP y PIM representan esa relación de maneras diferentes, el mapping debe conservarla explícitamente durante la transformación.

Sincronización bidireccional: definir quién puede escribir cada dato

Uno de los errores conceptuales más frecuentes es interpretar “integración bidireccional” como “todos los datos viajan en ambas direcciones”.

No funciona así.

Una integración puede ser bidireccional y, al mismo tiempo, establecer que determinados campos circulen exclusivamente en una dirección.

Supongamos que el ERP gobierna sku, información logística, unidad de medida y código de proveedor, mientras que el PIM gobierna nombre comercial, descripción, taxonomía e imágenes. Si alguien modifica una descripción dentro del PIM, puede ser necesario distribuir ese cambio hacia otros sistemas. Lo que no tendría sentido es que el ERP recibiera esa modificación y después la devolviera al PIM como si fuera una nueva actualización.

Ahí aparece el clásico loop:

ERP → PIM → ERP → PIM → ...

Por eso prefiero diseñar estas integraciones en términos de autoridad por dominio de información, y no solamente dibujando flechas entre aplicaciones.

Bidireccional no significa que todo viaje para ambos lados. Significa que existen flujos en ambas direcciones con reglas claras sobre quién puede modificar cada dato.

En código, además de comparar valores, puede ser necesario conservar datos como source, fecha de modificación, versión o identificador del evento. El integrador debería poder responder una pregunta básica: ¿este cambio nació en el sistema que estoy leyendo o estoy recibiendo de vuelta algo que yo mismo envié?

¿Polling, deltas, webhooks o un patrón híbrido?

No todas las integraciones pueden ser event-driven. Hay ERP heredados que ofrecen archivos programados, vistas SQL o endpoints que solamente permiten hacer consultas periódicas.

En esos escenarios, utilizar polling no es necesariamente un problema. El problema es hacer polling sin una estrategia incremental.

Consultar un catálogo completo cada pocos minutos es sencillo de programar, pero escala mal. Siempre que el sistema de origen lo permita, prefiero trabajar con un watermark —por ejemplo, updated_at— y consultar únicamente los registros modificados desde la última ejecución confirmada.

Incluso en ese esquema puede ser conveniente utilizar un pequeño solapamiento temporal y deduplicar los resultados. Si una ejecución terminó a las 10:00, la siguiente podría consultar nuevamente desde 09:58. Reprocesar de forma idempotente un pequeño conjunto de datos suele ser menos problemático que perder una actualización por una diferencia de reloj o por una operación confirmada justo sobre el límite temporal.

Cuando el sistema ofrece webhooks, podemos reaccionar prácticamente en tiempo real ante determinados eventos y evitar consultas permanentes.

Pero, para integraciones críticas, me gusta especialmente un tercer enfoque: webhooks para reaccionar rápido y reconciliación periódica para verificar que no se haya perdido nada.

El evento aporta velocidad. La reconciliación aporta seguridad.

Idempotencia: cómo reintentar sin duplicar productos

Supongamos una situación bastante simple: enviamos un producto al PIM, el servidor lo procesa correctamente, pero la conexión se interrumpe antes de que Node.js reciba la respuesta.

Desde el punto de vista del integrador, no sabemos qué pasó.

¿Repetimos la operación?

Si simplemente repetimos un POST, existe la posibilidad de crear un duplicado.

Ese es exactamente el tipo de problema que casi nunca aparece durante una demo y termina apareciendo en producción.

HTTP define una operación como idempotente cuando ejecutar varias veces la misma petición produce el mismo efecto esperado que ejecutarla una sola vez. Métodos como PUT tienen esa propiedad por definición, mientras que una operación no idempotente necesita precauciones adicionales antes de ser repetida.

Traducido a una integración ERP-PIM, antes de programar un retry tengo que saber cómo voy a determinar si la operación anterior realmente se ejecutó.

Dependiendo de la API, podemos resolverlo mediante un endpoint de upsert, una clave externa estable, una idempotency key cuando esté disponible o una consulta posterior que permita verificar el estado antes de volver a escribir.

Un retry sin idempotencia no es una estrategia de recuperación. Puede ser una estrategia para crear el mismo producto dos veces.

La regla es simple: el retry no debería ser una reacción ciega frente a cualquier error.

Timeouts, rate limiting y backoff: controlar el ritmo antes de saturar la API

Otra diferencia importante entre desarrollo y producción es que, en producción, las dependencias se degradan.

Un endpoint no necesita caerse completamente para generar un problema. Puede simplemente empezar a responder en 30 o 40 segundos. Si tenemos numerosos workers esperando indefinidamente, el problema de un sistema remoto termina propagándose a nuestro propio integrador.

Node.js permite utilizar AbortSignal.timeout() para abortar una operación después de un período determinado. En una integración real, los límites deberían ser explícitos y responder al comportamiento del sistema que estamos consumiendo.

El mismo criterio aplica a los reintentos. Un esquema simplificado podría ser:

const RETRYABLE = new Set([408, 429, 502, 503, 504]);

const sleep = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

async function requestWithRetry(url, options = {}, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    let response;

    try {
      response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(10_000)
      });
    } catch (error) {
      if (attempt === maxAttempts) throw error;

      const wait = Math.random() * Math.min(
        1000 * 2 ** (attempt - 1),
        15_000
      );

      await sleep(wait);
      continue;
    }

    if (response.ok) return response;

    if (!RETRYABLE.has(response.status)) {
      throw new Error(`Non-retryable HTTP ${response.status}`);
    }

    if (attempt === maxAttempts) {
      throw new Error(`Max attempts reached: HTTP ${response.status}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));

    const exponential = Math.min(
      1000 * 2 ** (attempt - 1),
      15_000
    );

    const wait = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : Math.random() * exponential;

    await sleep(wait);
  }
}

No tomaría este fragmento como una librería universal. Cada API tiene sus propias reglas. Lo importante son los principios que aparecen detrás: timeout explícito, cantidad limitada de intentos y espera progresiva entre reintentos.

A esto se suma el rate limiting. Cuando tenemos diez mil productos esperando, puede resultar tentador ejecutar un gran Promise.all() y dejar que Node.js procese todo lo más rápido posible.

Node.js probablemente pueda hacerlo. La API de destino puede no agradecerlo.

Sales Layer, por ejemplo, documenta límites de peticiones para proteger su infraestructura y recomienda controlar concurrencia, utilizar colas y reservar capacidad para reintentos.

El throughput de una integración no está definido por cuántas promesas podemos crear. Está limitado por el contrato y la capacidad de los sistemas que estamos conectando.

Por eso suelo preferir colas con concurrencia controlada. Si además aparece un 429, el cliente tiene que reducir la velocidad y respetar las indicaciones del servidor cuando estén disponibles.

En esos escenarios también es útil combinar exponential backoff con jitter: aumentar progresivamente la espera y agregar cierta aleatoriedad para evitar que numerosos procesos fallen al mismo tiempo y vuelvan a intentarlo exactamente juntos.

¿Qué errores se reintentan y cuáles deben detener el registro?

Una política del tipo catch → retry(3) parece práctica, pero mezcla problemas muy diferentes.

Un error transitorio puede desaparecer en segundos. Un payload inválido no va a corregirse porque lo enviemos cuatro veces.

Una clasificación básica puede ser esta:

Tipo de falloEjemplos habitualesAcción recomendada
Transitoriotimeout, 429, 502, 503, 504Reintentar con límite y backoff
Datos inválidos400, 422Detener el registro y revisar el payload
Autenticación o permisos401, 403Alertar y revisar configuración
Conflicto de estado409 o equivalenteConsultar estado y reconciliar
Error inesperadoRespuesta no contempladaRegistrar contexto y escalar

Los códigos concretos dependen de cada API y la documentación del proveedor siempre debe tener prioridad. Lo importante es distinguir entre errores recuperables y no recuperables.

Reintentar cinco veces un producto rechazado porque le falta un atributo obligatorio no hace que nuestra integración sea más resiliente. Solo genera cinco veces el mismo error.

Checkpoints y recuperación: cómo retomar sin procesar todo de nuevo

En procesos de alto volumen aparece otra pregunta: ¿desde dónde retomamos si algo falla?

Si lo único que guardamos es “el job falló”, probablemente tengamos que repetir demasiado trabajo.

Una integración robusta debería persistir información suficiente para reconstruir su estado: syncId, cursor de paginación, último lote confirmado, última entidad procesada y timestamp de la ejecución, entre otros datos relevantes.

El detalle importante es cuándo actualizamos ese estado. El checkpoint debería avanzar después de confirmar una operación, no antes.

Esto cobra especial importancia con APIs paginadas. Akeneo, por ejemplo, dispone de mecanismos como search-after para recorrer conjuntos grandes de entidades mediante cursores en lugar de depender únicamente de un offset tradicional.

El principio, de todos modos, no depende de una plataforma específica: hay que persistir suficiente contexto para continuar un proceso sin tener que adivinar dónde quedó.

Recuperarse no significa volver a ejecutar todo. Una buena integración sabe cuál fue el último estado confirmado y puede continuar desde allí.

Validar antes de enviar y reconciliar después de sincronizar

Antes de llamar a una API destino, quiero saber si los datos cumplen al menos las condiciones que yo mismo puedo validar.

¿Existe el identificador esperado? ¿La familia es válida? ¿Las relaciones padre-hijo pueden resolverse? ¿Los tipos son correctos? ¿Los valores pertenecen al vocabulario admitido?

Ese control evita enviar errores previsibles hacia sistemas externos.

Ya desarrollé este tema con más detalle al hablar de pipelines de validación en Node.js, donde el objetivo era detectar problemas de completitud, formato, consistencia y reglas de negocio antes de que llegaran al PIM.

Pero existe otra validación que considero igual de importante y que muchas veces queda fuera del diseño: reconciliar después de la sincronización.

Si el ERP entregó 5.000 productos, el integrador procesó 5.000 registros y el PIM terminó con 4.987 actualizaciones válidas, la ejecución no debería figurar simplemente como “success”.

Procesado no es sinónimo de confirmado.

La reconciliación puede comparar identificadores esperados contra aceptados, cantidades por lote, respuestas parciales o atributos críticos. El objetivo es detectar diferencias dentro del propio proceso y no varios días más tarde, cuando alguien encuentra un producto incorrecto en el eCommerce.

En una integración, “enviamos 5.000 productos” no equivale a “5.000 productos quedaron correctamente sincronizados”. La diferencia entre ambas afirmaciones es la reconciliación.

Observabilidad: poder reconstruir el recorrido de cada producto

Cuando una integración falla, saber solamente que “hubo un error” sirve de poco.

Necesito conocer qué ejecución estaba corriendo, qué producto estaba involucrado, de dónde venía, hacia dónde iba, qué operación estaba intentando, cuánto tardó, cuántas veces se reintentó y qué contestó el sistema destino.

Ahí entra la observabilidad.

Un log estructurado puede conservar información como esta:

logger.info({
  syncId,
  entityId: product.sku,
  source: "erp",
  destination: "pim",
  operation: "upsert",
  attempt,
  durationMs,
  status: "success"
});

A partir de ese contexto podemos construir métricas útiles: productos procesados por minuto, porcentaje de errores, cantidad de retries, respuestas 429, latencia media, duración de la sincronización o registros pendientes de reconciliación.

Ya dediqué otro artículo a los errores silenciosos y a la necesidad de generar logs que permitan seguir una entidad de punta a punta.

La idea fundamental sigue siendo la misma: los logs no deberían contar solamente que algo falló. Deberían permitir reconstruir qué ocurrió.

Lo que muestran los proyectos reales de integración ERP-PIM

Estos patrones no aparecen solamente leyendo documentación.

En CRITERIA trabajamos, por ejemplo, con una operación multicanal en la que desarrollamos con JavaScript y Node.js un proceso que consulta periódicamente el ERP, detecta productos nuevos, los incorpora al PIM y posteriormente distribuye deltas —productos nuevos o modificados— hacia otros componentes del ecosistema.

En otro proyecto multimarca, la integración requirió varios scripts Node.js coordinados mediante un orquestador. Había procesos para crear y actualizar productos y variantes, intercambiar información con el ERP, generar identificadores externos y distribuir información hacia distintos sistemas.

Cuando un ecosistema llega a ese nivel, un “script que mueve JSON” deja de ser suficiente.

Lo que estamos construyendo se parece mucho más a un pequeño sistema distribuido que necesita administrar identidad, estado, errores, trazabilidad y recuperación.

Checklist técnico antes de salir a producción

Antes de dejar una integración ERP-PIM operando sin supervisión permanente, revisaría al menos estos puntos:

  • Está definido qué sistema gobierna cada dato.
  • Existe una estrategia clara para relacionar IDs de ERP, PIM y otros sistemas.
  • Los mappings fueron probados con productos simples, variantes y excepciones.
  • Los tipos de datos y vocabularios se validan antes del envío.
  • El volumen utilizado en las pruebas representa razonablemente el volumen productivo.
  • Existen timeouts explícitos.
  • La concurrencia y el ritmo de peticiones están controlados.
  • Los retries distinguen errores transitorios de errores permanentes.
  • Las operaciones repetibles son idempotentes o cuentan con mecanismos de deduplicación.
  • Se probaron deliberadamente 429, timeouts, errores 5xx y fallos parciales.
  • Existen checkpoints persistentes.
  • Se comprobó que un proceso puede reiniciarse desde un punto intermedio.
  • Los logs permiten seguir una entidad de punta a punta.
  • Existen métricas y alertas para detectar degradaciones.
  • Hay una reconciliación posterior entre lo que debía sincronizarse y lo que efectivamente quedó sincronizado.

Ese checklist no evita todos los problemas. Lo que hace es evitar que cada problema inesperado obligue a improvisar.

¿Cuándo está realmente lista una integración ERP-PIM?

Para mí, una integración ERP-PIM está preparada para producción cuando podemos responder con claridad tres preguntas: qué ocurrió, qué quedó pendiente y cómo podemos recuperarlo.

Que los endpoints respondan correctamente es apenas el comienzo. Una integración capaz de sostener un catálogo necesita identidad estable, autoridad del dato, procesamiento incremental, idempotencia, control de velocidad, retries inteligentes, checkpoints, validación, reconciliación y observabilidad.

Cada pieza parece pequeña de forma aislada. En conjunto son las que evitan que un problema transitorio termine convertido en productos duplicados, actualizaciones perdidas o información inconsistente distribuida por todos los canales.

Node.js nos da herramientas muy buenas para construir esta capa, pero el valor no está simplemente en usar fetch, promesas o un cron. El salto real aparece cuando diseñamos el integrador como una pieza de infraestructura que sabemos que alguna vez va a fallar y que, cuando eso ocurra, tiene que poder identificar el problema, conservar su estado y recuperarse sin comprometer los datos.

Foto del avatar

Desarrollador Node.js Senior en CRITERIA Smart Cataloging. Responsable de las integraciones API REST entre plataformas PIM y sistemas de eCommerce, ERP, marketplaces y puntos de venta. Construye los puentes técnicos que conectan el dato de producto con cada canal de distribución.