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.