smplx.
Shopify Architecture

Third-Party Integrations in Shopify: ERP, CRM and More [2026]

Claudio Gerlich··13 min

In our Shopify Architecture Guide, we cover the fundamentals. Today: The visible complexity. Most shops are not "just Shopify" — they're Shopify + ERP + CRM + Shipping + Payments.

At smplx., we've been connecting Shopify with systems like SAP, Xentral, HubSpot, and Salesforce since 2020. We know: The integration is the art.

The Three Integration Patterns

Pattern 1: Webhook-Driven (Event-Based)

Architecture:

Shopify Event -> Webhook -> Your System -> External Systems

Example: Order created

1. Customer buys on Shopify
2. Shopify sends webhook (order/create) to your app
3. Your app processes webhook async
4. Your app tells ERP: "New order!"
5. ERP processes

Pros:

  • Real-time reaction to events
  • Asynchronous = fast for customers
  • Scalable
  • GDPR-friendly (data doesn't flow through the frontend)

Cons:

  • Requires a reliable backend
  • Webhook retry handling needed
  • Debugging is harder

Pattern 2: Polling (Querying)

Architecture:

Your System -> Timer (every X seconds) -> Shopify API -> Processing

Example:

// Every evening: Fetch all new orders
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 hours

Pros:

  • Easy to understand
  • No webhook setup needed
  • Easier debugging

Cons:

  • Delay (until next poll)
  • Wastes API calls
  • Rate limits problematic
  • Not real-time

Pattern 3: Middleware (Orchestration)

Architecture:

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

Example: helpyourself (Medical Devices)

helpyourself needed to connect Shopify with iLabServer (medical ERP). Very sensitive data.

Shopify Order
    |
Webhook -> helpyourself Middleware
    ├─ Validates data
    ├─ Masks sensitive info
    └─ Sends to iLabServer
         ├─ Processes order
         └─ Sends confirmation back

Pros:

  • Central control
  • Data privacy logic in one place
  • Transformation possible
  • Audit logging easy

Cons:

  • More complex infrastructure
  • Single point of failure (if middleware is down)
  • More to maintain

Concrete Integrations

1. ERP Integration (Inventory Sync)

Scenario: Shopify -> Xentral (Cloud ERP)

Challenge:

  • Inventory in Shopify MUST be synced with Xentral
  • When Xentral changes stock -> Update Shopify
  • Reverse direction: When Shopify sale -> Xentral needs to know

Solution: Bidirectional Webhooks

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

  // Respond immediately (important!)
  res.status(202).send('Processing...');

  // Async: Send to 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);

  // Send to 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: Inventory changes
app.post('/webhooks/xentral/inventory-change', async (req, res) => {
  const { sku, quantity } = req.body;

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

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

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

  // Map SKU to 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;

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

Important: Rate Limits

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

// Implement retry logic
const retryConfig = {
  retries: 3,
  retryDelay: exponentialBackoff
};

xentral.interceptors.response.use(null, async (error) => {
  if (error.response.status === 429) {
    // Rate limited! Wait exponentially longer
    const delay = Math.pow(2, attempt) * 1000;
    await new Promise(resolve => setTimeout(resolve, delay));
    return retryRequest(error.config);
  }
});

2. CRM Integration (HubSpot)

Scenario: Shopify Customer -> HubSpot Contact

Challenge:

  • New customers in Shopify -> HubSpot
  • Order data -> HubSpot Deal
  • Further interactions synced

Solution: Event-Driven with HubSpot API

// 1. Customer created -> 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 {
    // Create HubSpot Contact
    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',
        // Custom Properties
        shopify_customer_id: customerId,
        shopify_total_spent: customer.total_spent,
        shopify_order_count: customer.orders_count
      }
    });

    // Store HubSpot ID back in 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; // Queue will retry
  }
});

// 3. Order -> 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. Order to Deal in HubSpot
queue.process('sync-order-to-hubspot', async (job) => {
  const { orderId, customerId, amount } = job.data;

  // Get HubSpot Contact ID from 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');
  }

  // Create 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. Payment Provider Integration

Scenario: Stripe/Klarna with Shopify

Challenge:

  • Payment authorized in Shopify
  • Must be marked in payment provider
  • Disputes/refunds should sync bidirectionally

Shopify makes it easy:

  • Built-in Stripe, Klarna, Adyen
  • Automatic webhooks
  • No custom code needed (usually)

But when custom integration is needed:

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

  // Find the Shopify Order
  const shopifyOrderId = charge.metadata.shopify_order_id;

  // Mark as Paid in Shopify
  await shopify.graphql(`
    mutation {
      orderMarkAsPaid(input: { id: "gid://shopify/Order/${shopifyOrderId}" }) {
        order {
          id
          displayFinancialStatus
        }
      }
    }
  `);
});

// Shopify -> Stripe (Refund)
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) // In cents
    });
  }
});

Real-World: helpyourself Case Study

helpyourself is a medical devices shop with GDPR-critical requirements.

Challenge:

  • Customer data must flow GDPR-compliantly
  • Orders must go to iLabServer (proprietary ERP)
  • No data loops between systems
  • Audit logging for medical compliance

Solution: Central Middleware

// helpyourself Middleware Architecture

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

  // Webhook from Shopify arrives
  async handleOrderWebhook(order) {
    // 1. Validate order
    if (!this.validateOrder(order)) {
      throw new Error('Invalid order structure');
    }

    // 2. Data mapping with data protection
    const transformedOrder = this.transformOrderForILab(order);

    // 3. Protect sensitive data
    const maskedOrder = this.maskSensitiveData(transformedOrder);

    // 4. Send to iLab with retry
    try {
      const ilabResult = await this.ilab.createOrder(maskedOrder);

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

      // 6. Audit Log
      this.auditLog('order_synced', {
        shopify_order_id: order.id,
        ilab_order_id: ilabResult.id,
        timestamp: new Date(),
        gdpr_compliant: true
      });

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

      // Queue for retry
      await this.queue.add('retry_order_sync', { orderId: order.id });
    }
  }

  // Data protection transformations
  maskSensitiveData(order) {
    return {
      ...order,
      customer: {
        // Only necessary data
        email: order.customer.email,
        phone: order.customer.phone,
        // DO NOT send credit card data
        // Shopify doesn't store them anyway
      },
      billing_address: {
        // Full address
      },
      shipping_address: {
        // Full address
      }
      // But no IPs, cookies, browser fingerprints!
    };
  }

  // GDPR data deletion
  async deleteCustomerData(customerId) {
    // 1. Delete from Shopify
    await this.shopify.deleteCustomer(customerId);

    // 2. Delete from iLab (if possible)
    try {
      await this.ilab.deleteCustomer(customerId);
    } catch (error) {
      // If iLab can't delete: Log for GDPR
      this.auditLog('customer_delete_failed', {
        customerId,
        system: 'ilab',
        reason: error.message
      });
    }

    // 3. Delete middleware logs (except for legally required retention)
    await this.database.deleteCustomerLogs(customerId);
  }
}

Important: Medical data is strictly regulated. helpyourself needed:

  • Audit logs for every operation
  • Encryption in transit (TLS)
  • Encryption at rest
  • Data residency (GDPR: EU data stays in the EU)
  • Deletion workflows

GDPR in Integrations

Your responsibility:

  • Only share data with explicit consent
  • Customer can delete data ("Right to be Forgotten")
  • Don't store data longer than necessary
  • Be transparent about data flow

Practically:

// WRONG: Share data automatically
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;
  // Send everything to Facebook Pixel, Google Analytics, etc.
  await sendToFacebook(order);
  await sendToGoogle(order);
});

// RIGHT: Only with consent
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;

  if (order.customer.accepts_marketing) {
    // Only then share data
    await sendToFacebook(order);
  }

  if (customer.accepts_email_marketing) {
    // Only then to email service
    await sendToMailchimp(order.customer);
  }
});

// Deletion workflow
app.post('/api/delete-my-data', async (req, res) => {
  const { customerId } = req.body;

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

  // 2. All third-party services
  await facebook.deleteCustomer(customerId);
  await google.deleteCustomer(customerId);
  await mailchimp.deleteCustomer(customerId);

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

Integration Checklist

  • Pattern chosen: Webhook, Polling, or Middleware?
  • Rate limits handled? (Retry logic)
  • Webhook idempotency implemented? (No duplicate processing)
  • Error logging on both sides? (Debugging later)
  • GDPR data flow checked?
  • Data formats validated? (JSON Schema)
  • Sensitive data masked?
  • Monitoring active? (Health checks)
  • Failure case handling? (What if external API is down?)
  • Audit logging?

About the Author

Claudio Gerlich is Technical Architect at smplx. and has specialized in Shopify integrations since 2020. With helpyourself, we proved that even medically regulated data can flow securely through webhook integrations — when the architecture is right.

Integrations are not easy. But with the right patterns, they become manageable.

smplx. is a Shopify Technical Partner (since 2020) based in Muensterland, NRW.