smplx.
Arquitectura Shopify

Integraciones de terceros en Shopify: ERP, CRM y más [2026]

Claudio Gerlich··13 min

En nuestra Guía de Arquitectura Shopify cubrimos los fundamentos. Hoy: La complejidad visible. La mayoría de las tiendas no son "solo Shopify" — son Shopify + ERP + CRM + Envíos + Pagos.

En smplx. llevamos desde 2020 conectando Shopify con sistemas como SAP, Xentral, HubSpot y Salesforce. Sabemos: La integración es el arte.

Los tres patrones de integración

Patrón 1: Webhook-Driven (basado en eventos)

Arquitectura:

Evento Shopify -> Webhook -> Tu sistema -> Sistemas externos

Ejemplo: Pedido creado

1. Cliente compra en Shopify
2. Shopify envía webhook (order/create) a tu app
3. Tu app procesa el webhook de forma asíncrona
4. Tu app dice al ERP: "¡Nuevo pedido!"
5. ERP procesa

Pros:

  • Reacción en tiempo real a eventos
  • Asíncrono = rápido para los clientes
  • Escalable
  • Compatible con RGPD (los datos no pasan por el frontend)

Contras:

  • Requiere un backend fiable
  • Necesita manejo de reintentos de webhooks
  • El debugging es más difícil

Patrón 2: Polling (consulta)

Arquitectura:

Tu sistema -> Temporizador (cada X segundos) -> Shopify API -> Procesamiento

Ejemplo:

// Cada noche: Obtener todos los pedidos nuevos
setInterval(async () => {
  const orders = await shopify.get('/admin/api/2024-01/orders.json?status=any');

  for (const order of orders) {
    if (!alreadyProcessed(order.id)) {
      await sendToERP(order);
    }
  }
}, 86400000); // 24 horas

Pros:

  • Fácil de entender
  • No necesita configuración de webhooks
  • Debugging más fácil

Contras:

  • Retraso (hasta el siguiente poll)
  • Desperdicia llamadas API
  • Rate limits problemáticos
  • No es tiempo real

Patrón 3: Middleware (orquestación)

Arquitectura:

Shopify <-> Middleware <-> ERP
  |          |          |
 API       GraphQL     API

Ejemplo: helpyourself (dispositivos médicos)

helpyourself necesitaba conectar Shopify con iLabServer (ERP médico). Datos muy sensibles.

Pedido Shopify
    |
Webhook -> helpyourself Middleware
    ├─ Valida datos
    ├─ Enmascara info sensible
    └─ Envía a iLabServer
         ├─ Procesa pedido
         └─ Envía confirmación de vuelta

Pros:

  • Control centralizado
  • Lógica de protección de datos en un solo lugar
  • Transformación posible
  • Registro de auditoría fácil

Contras:

  • Infraestructura más compleja
  • Punto único de fallo (si el middleware cae)
  • Más que mantener

Integraciones concretas

1. Integración ERP (sincronización de inventario)

Escenario: Shopify -> Xentral (Cloud ERP)

Desafío:

  • El inventario en Shopify DEBE estar sincronizado con Xentral
  • Cuando Xentral cambia el stock -> Actualizar Shopify
  • Dirección inversa: Cuando hay una venta en Shopify -> Xentral debe saberlo

Solución: Webhooks bidireccionales

// 1. Shopify Webhook: Pedido creado
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;

  // Responder inmediatamente (¡importante!)
  res.status(202).send('Processing...');

  // Async: Enviar a Xentral
  const job = await queue.add('sync-to-erp', { orderId: order.id });
});

// 2. Background Job
queue.process('sync-to-erp', async (job) => {
  const { orderId } = job.data;
  const order = await shopify.rest.Order.find(orderId);

  // Enviar a Xentral
  const xentralResult = await xentral.api.post('/orders', {
    external_order_id: order.id,
    customer: order.customer,
    items: order.line_items.map(item => ({
      sku: item.sku,
      quantity: item.quantity,
      price: item.price
    })),
    total: order.total_price
  });

  // Logging
  await logSync('order', orderId, 'SUCCESS');
});

// 3. Xentral Webhook: Cambio de inventario
app.post('/webhooks/xentral/inventory-change', async (req, res) => {
  const { sku, quantity } = req.body;

  res.status(202).send('Syncing...');

  // Actualizar inventario en Shopify
  const job = await queue.add('update-inventory', { sku, quantity });
});

// 4. Background Job para inventario
queue.process('update-inventory', async (job) => {
  const { sku, quantity } = job.data;

  // Mapear SKU a Shopify Variant
  const variant = await shopify.graphql(`
    query {
      productVariants(first: 1, query: "sku:${sku}") {
        edges {
          node {
            id
            sku
          }
        }
      }
    }
  `);

  if (variant.edges.length > 0) {
    const variantId = variant.edges[0].node.id;

    // Actualizar inventario
    await shopify.graphql(`
      mutation {
        inventorySetQuantities(input: {
          ignoreUnknownLocations: true
          quantities: [{
            inventoryItemId: "${variantId}"
            availableQuantity: ${quantity}
          }]
        }) {
          inventoryItems {
            id
            sku
          }
        }
      }
    `);
  }
});

Importante: Rate Limits

// Xentral tiene rate limits
const xentral = axios.create({
  baseURL: 'https://xentral.api.com',
  headers: { 'Authorization': `Bearer ${XENTRAL_TOKEN}` }
});

// Implementar lógica de reintentos
const retryConfig = {
  retries: 3,
  retryDelay: exponentialBackoff
};

xentral.interceptors.response.use(null, async (error) => {
  if (error.response.status === 429) {
    // ¡Rate limited! Esperar exponencialmente más tiempo
    const delay = Math.pow(2, attempt) * 1000;
    await new Promise(resolve => setTimeout(resolve, delay));
    return retryRequest(error.config);
  }
});

2. Integración CRM (HubSpot)

Escenario: Shopify Customer -> HubSpot Contact

Desafío:

  • Nuevos clientes en Shopify -> HubSpot
  • Datos de pedidos -> HubSpot Deal
  • Más interacciones sincronizadas

Solución: Event-Driven con HubSpot API

// 1. Cliente creado -> HubSpot Contact
app.post('/webhooks/customers/create', async (req, res) => {
  const customer = req.body;

  res.status(202).send('Processing...');

  const job = await queue.add('sync-customer-to-hubspot', {
    customerId: customer.id,
    email: customer.email
  });
});

// 2. Background Job
queue.process('sync-customer-to-hubspot', async (job) => {
  const { customerId, email } = job.data;

  try {
    // Crear contacto en HubSpot
    const contact = await hubspot.post('/crm/v3/objects/contacts', {
      associations: [],
      properties: {
        firstname: customer.first_name,
        lastname: customer.last_name,
        email: customer.email,
        phone: customer.phone,
        lifecyclestage: 'customer',
        // Propiedades personalizadas
        shopify_customer_id: customerId,
        shopify_total_spent: customer.total_spent,
        shopify_order_count: customer.orders_count
      }
    });

    // Guardar ID de HubSpot de vuelta en Shopify Metafield
    await shopify.graphql(`
      mutation {
        metafieldsSet(ownerId: "gid://shopify/Customer/${customerId}", metafields: {
          key: "hubspot_contact_id"
          value: "${contact.id}"
          type: "single_line_text"
        }) {
          metafields { id }
        }
      }
    `);

  } catch (error) {
    logger.error('HubSpot sync failed', { email, error: error.message });
    throw error; // La cola hará reintento
  }
});

// 3. Pedido -> HubSpot Deal
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;

  res.status(202).send('Processing...');

  const job = await queue.add('sync-order-to-hubspot', {
    orderId: order.id,
    customerId: order.customer.id,
    amount: order.total_price
  });
});

// 4. Pedido a Deal en HubSpot
queue.process('sync-order-to-hubspot', async (job) => {
  const { orderId, customerId, amount } = job.data;

  // Obtener HubSpot Contact ID del Metafield
  const customer = await shopify.graphql(`
    query {
      customer(id: "gid://shopify/Customer/${customerId}") {
        hubspotContactId: metafield(namespace: "custom", key: "hubspot_contact_id") {
          value
        }
      }
    }
  `);

  const hubspotContactId = customer.hubspotContactId?.value;

  if (!hubspotContactId) {
    throw new Error('HubSpot Contact ID not found');
  }

  // Crear Deal
  const deal = await hubspot.post('/crm/v3/objects/deals', {
    associations: [{
      types: [{ associationType: 'contact_to_deal' }],
      id: hubspotContactId
    }],
    properties: {
      dealname: `Order #${orderId}`,
      dealstage: 'closedwon',
      amount: amount,
      closedate: Date.now(),
      shopify_order_id: orderId
    }
  });
});

3. Integración de proveedor de pagos

Escenario: Stripe/Klarna con Shopify

Desafío:

  • Pago autorizado en Shopify
  • Debe marcarse en el proveedor de pagos
  • Disputas/reembolsos deben sincronizarse bidireccionalmente

Shopify lo facilita:

  • Stripe, Klarna, Adyen integrados
  • Webhooks automáticos
  • No se necesita código personalizado (normalmente)

Pero cuando se necesita integración personalizada:

// Stripe -> Shopify (Pago exitoso)
app.post('/webhooks/stripe/charge.succeeded', async (req, res) => {
  const event = req.body;
  const charge = event.data.object;

  // Encontrar el pedido de Shopify
  const shopifyOrderId = charge.metadata.shopify_order_id;

  // Marcar como pagado en Shopify
  await shopify.graphql(`
    mutation {
      orderMarkAsPaid(input: { id: "gid://shopify/Order/${shopifyOrderId}" }) {
        order {
          id
          displayFinancialStatus
        }
      }
    }
  `);
});

// Shopify -> Stripe (Reembolso)
app.post('/webhooks/shopify/refunds/create', async (req, res) => {
  const { refund, orders } = req.body;
  const order = orders[0];

  const stripeChargeId = order.metafields?.payment?.stripe_charge_id?.value;

  if (stripeChargeId) {
    await stripe.refunds.create({
      charge: stripeChargeId,
      amount: Math.round(refund.transactions[0].amount * 100) // En centavos
    });
  }
});

Caso real: Caso de estudio helpyourself

helpyourself es una tienda de dispositivos médicos con requisitos críticos de RGPD.

Desafío:

  • Los datos de clientes deben fluir conforme al RGPD
  • Los pedidos deben ir a iLabServer (ERP propietario)
  • Sin bucles de datos entre sistemas
  • Registro de auditoría para cumplimiento médico

Solución: Middleware central

// Arquitectura del Middleware de helpyourself

class HelpyourselfMiddleware {
  constructor() {
    this.shopify = new ShopifyAPI({ ... });
    this.ilab = new iLabServerAPI({ ... });
  }

  // Llega webhook de Shopify
  async handleOrderWebhook(order) {
    // 1. Validar pedido
    if (!this.validateOrder(order)) {
      throw new Error('Invalid order structure');
    }

    // 2. Mapeo de datos con protección de datos
    const transformedOrder = this.transformOrderForILab(order);

    // 3. Proteger datos sensibles
    const maskedOrder = this.maskSensitiveData(transformedOrder);

    // 4. Enviar a iLab con reintento
    try {
      const ilabResult = await this.ilab.createOrder(maskedOrder);

      // 5. Guardar ID de iLab de vuelta en Shopify
      await this.shopify.updateOrderMetafield(order.id, {
        ilab_order_id: ilabResult.id,
        sync_status: 'success',
        sync_timestamp: new Date().toISOString()
      });

      // 6. Registro de auditoría
      this.auditLog('order_synced', {
        shopify_order_id: order.id,
        ilab_order_id: ilabResult.id,
        timestamp: new Date(),
        gdpr_compliant: true
      });

    } catch (error) {
      // 7. Manejo de errores
      logger.error('Order sync failed', {
        orderId: order.id,
        error: error.message
      });

      // Cola para reintento
      await this.queue.add('retry_order_sync', { orderId: order.id });
    }
  }

  // Transformaciones de protección de datos
  maskSensitiveData(order) {
    return {
      ...order,
      customer: {
        // Solo datos necesarios
        email: order.customer.email,
        phone: order.customer.phone,
        // NO enviar datos de tarjeta de crédito
        // Shopify no los almacena de todas formas
      },
      billing_address: {
        // Dirección completa
      },
      shipping_address: {
        // Dirección completa
      }
      // ¡Pero sin IPs, cookies, ni huellas digitales del navegador!
    };
  }

  // Eliminación de datos RGPD
  async deleteCustomerData(customerId) {
    // 1. Eliminar de Shopify
    await this.shopify.deleteCustomer(customerId);

    // 2. Eliminar de iLab (si es posible)
    try {
      await this.ilab.deleteCustomer(customerId);
    } catch (error) {
      // Si iLab no puede eliminar: Registrar para RGPD
      this.auditLog('customer_delete_failed', {
        customerId,
        system: 'ilab',
        reason: error.message
      });
    }

    // 3. Eliminar logs del middleware (excepto por retención legalmente requerida)
    await this.database.deleteCustomerLogs(customerId);
  }
}

Importante: Los datos médicos están estrictamente regulados. helpyourself necesitaba:

  • Registros de auditoría para cada operación
  • Cifrado en tránsito (TLS)
  • Cifrado en reposo
  • Residencia de datos (RGPD: los datos de la UE permanecen en la UE)
  • Flujos de eliminación

RGPD en integraciones

Tu responsabilidad:

  • Solo compartir datos con consentimiento explícito
  • El cliente puede eliminar sus datos ("Derecho al olvido")
  • No almacenar datos más tiempo del necesario
  • Ser transparente sobre el flujo de datos

En la práctica:

// INCORRECTO: Compartir datos automáticamente
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;
  // Enviar todo a Facebook Pixel, Google Analytics, etc.
  await sendToFacebook(order);
  await sendToGoogle(order);
});

// CORRECTO: Solo con consentimiento
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;

  if (order.customer.accepts_marketing) {
    // Solo entonces compartir datos
    await sendToFacebook(order);
  }

  if (customer.accepts_email_marketing) {
    // Solo entonces al servicio de email
    await sendToMailchimp(order.customer);
  }
});

// Flujo de eliminación
app.post('/api/delete-my-data', async (req, res) => {
  const { customerId } = req.body;

  // 1. Shopify
  await shopify.deleteCustomer(customerId);

  // 2. Todos los servicios de terceros
  await facebook.deleteCustomer(customerId);
  await google.deleteCustomer(customerId);
  await mailchimp.deleteCustomer(customerId);

  // 3. Tus logs
  await database.deleteCustomerData(customerId);
});

Checklist de integración

  • ¿Patrón elegido: Webhook, Polling o Middleware?
  • ¿Rate limits manejados? (Lógica de reintento)
  • ¿Idempotencia de webhooks implementada? (Sin procesamiento duplicado)
  • ¿Registro de errores en ambos lados? (Para debugging posterior)
  • ¿Flujo de datos RGPD verificado?
  • ¿Formatos de datos validados? (JSON Schema)
  • ¿Datos sensibles enmascarados?
  • ¿Monitoreo activo? (Health checks)
  • ¿Manejo de casos de fallo? (¿Qué pasa si la API externa está caída?)
  • ¿Registro de auditoría?

Sobre el autor

Claudio Gerlich es Technical Architect en smplx. y está especializado en integraciones Shopify desde 2020. Con helpyourself demostramos que incluso los datos médicos regulados pueden fluir de forma segura a través de integraciones de webhooks — cuando la arquitectura es la correcta.

Las integraciones no son fáciles. Pero con los patrones adecuados, se vuelven manejables.

smplx. es Shopify Technical Partner (desde 2020) con sede en Muensterland, NRW.