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.