You have your store on Shopify and your back-office on Odoo. Two systems running in parallel, manual stock updates, orders that need to be copied by hand, customers duplicated in both databases. This is not sustainable. The integration between Odoo and Shopify is one of the most requested automation projects in B2C and B2B e-commerce — and also one that requires the most technical care to get right.
This guide covers every available method with real code examples, the most common mistakes, and the recommended architecture for high-volume stores.
Why integrate Odoo with Shopify (instead of managing them separately)
Manual management between two systems seems manageable until it scales. Beyond a certain volume, the hidden costs are enormous:
- Stock errors: A product sold on Shopify that does not deduct from Odoo generates overselling. A returned order not recorded in Odoo creates ghost inventory.
- Operational time: Updating prices, creating sales orders, registering customers — every operation duplicated across two systems is wasted time.
- Data inconsistency: Customers have different addresses in Shopify and Odoo. Accounting does not match actual sales.
- Blocked scalability: With 50 orders a day you can manage manually. With 500, it is impossible.
Integration solves all of this: a single data flow that keeps both systems synchronized in real time (or near real time) without manual intervention.
What data do you need to synchronize
Before choosing an integration method, define exactly which flows you need. The most common ones are:
Inventory (Odoo → Shopify)
Odoo is the source of truth for stock. Every time stock changes in Odoo (purchase, sale, adjustment or return), Shopify must be updated. Synchronization can be real-time (webhook) or periodic (every 5–15 minutes).
Orders (Shopify → Odoo)
Every confirmed order in Shopify must create a sales order in Odoo. This includes product lines, discounts, taxes, shipping address and customer data.
Customers (bidirectional)
A first-time buyer on Shopify must be created as a partner in Odoo. If that customer already has a history in Odoo (for example, through a prior B2B channel), you must avoid duplicates using email or VAT as the deduplication key.
Prices and catalogue (Odoo → Shopify)
If you manage pricing in Odoo (via price lists, B2B rates, volume discounts), you need to push them to Shopify. This synchronization is usually less frequent (daily or on explicit change).
Accounting (Shopify → Odoo)
Shopify Payments payments, refunds and fees must be reflected in Odoo so the accounting is complete. This is typically done through automatic journal entries based on Shopify payouts.
Method 1: Odoo native API + Shopify Webhooks
This is the most robust and flexible method. It requires development but gives you full control over the synchronization logic.
Flow architecture
[Shopify] --webhook--> [Middleware API] --XML-RPC/REST--> [Odoo]
[Odoo] --webhook--> [Middleware API] --Admin API-----> [Shopify]
The middleware (a Node.js or Python service) acts as orchestrator: it receives webhooks from both systems, transforms the data and writes to the other system.
Receiving Shopify webhooks in Python
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
import xmlrpc.client
import os
app = Flask(__name__)
SHOPIFY_SECRET = os.environ['SHOPIFY_WEBHOOK_SECRET']
ODOO_URL = os.environ['ODOO_URL']
ODOO_DB = os.environ['ODOO_DB']
ODOO_USER = os.environ['ODOO_USER']
ODOO_PASSWORD = os.environ['ODOO_PASSWORD']
def verify_shopify_webhook(data, hmac_header):
"""Verifies the webhook genuinely comes from Shopify."""
digest = hmac.new(
SHOPIFY_SECRET.encode('utf-8'),
data,
hashlib.sha256
).digest()
computed = base64.b64encode(digest).decode('utf-8')
return hmac.compare_digest(computed, hmac_header)
def get_odoo_connection():
"""Returns an authenticated Odoo connection."""
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
return uid, models
@app.route('/webhooks/shopify/orders/create', methods=['POST'])
def shopify_order_created():
# Verify signature
hmac_header = request.headers.get('X-Shopify-Hmac-Sha256')
if not verify_shopify_webhook(request.data, hmac_header):
return jsonify({'error': 'Unauthorized'}), 401
order = request.get_json()
uid, models = get_odoo_connection()
# Find or create customer in Odoo
email = order['email']
partner_ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'search',
[[['email', '=', email]]]
)
if not partner_ids:
partner_id = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'create',
[{
'name': f"{order['billing_address']['first_name']} {order['billing_address']['last_name']}",
'email': email,
'phone': order['billing_address'].get('phone', ''),
'street': order['billing_address'].get('address1', ''),
'city': order['billing_address'].get('city', ''),
'zip': order['billing_address'].get('zip', ''),
'country_id': get_country_id(models, uid, order['billing_address'].get('country_code')),
}]
)
else:
partner_id = partner_ids[0]
# Create sales order in Odoo
order_lines = []
for item in order['line_items']:
product_id = find_odoo_product(models, uid, item['sku'])
if product_id:
order_lines.append((0, 0, {
'product_id': product_id,
'product_uom_qty': item['quantity'],
'price_unit': float(item['price']),
'name': item['name'],
}))
sale_order_id = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'sale.order', 'create',
[{
'partner_id': partner_id,
'order_line': order_lines,
'client_order_ref': order['name'], # Shopify order number
'note': f"Shopify Order #{order['order_number']}",
}]
)
# Confirm the order automatically
models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'sale.order', 'action_confirm',
[[sale_order_id]]
)
return jsonify({'odoo_order_id': sale_order_id}), 200
def find_odoo_product(models, uid, sku):
"""Finds a product in Odoo by internal reference (SKU)."""
product_ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'product.product', 'search',
[[['default_code', '=', sku]]]
)
return product_ids[0] if product_ids else None
Synchronizing stock from Odoo to Shopify in Node.js
import axios from 'axios';
const SHOPIFY_STORE = process.env.SHOPIFY_STORE; // yourstore.myshopify.com
const SHOPIFY_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const shopifyClient = axios.create({
baseURL: `https://${SHOPIFY_STORE}/admin/api/2024-01`,
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
'Content-Type': 'application/json',
},
});
async function syncStockToShopify(odooProduct) {
const { sku, qty_available, shopify_variant_id, shopify_location_id } = odooProduct;
if (!shopify_variant_id) {
console.warn(`Product ${sku} has no shopify_variant_id mapped`);
return;
}
// Get inventory_item_id from Shopify
const variantRes = await shopifyClient.get(
`/variants/${shopify_variant_id}.json`
);
const inventoryItemId = variantRes.data.variant.inventory_item_id;
// Update stock
const response = await shopifyClient.post('/inventory_levels/set.json', {
inventory_item_id: inventoryItemId,
location_id: shopify_location_id,
available: Math.max(0, Math.floor(qty_available)),
});
console.log(`Stock updated: ${sku} → ${qty_available} units`);
return response.data;
}
// Odoo webhook when stock changes (via automated action)
app.post('/webhooks/odoo/stock-update', async (req, res) => {
const { products } = req.body;
const results = await Promise.allSettled(
products.map(product => syncStockToShopify(product))
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.error(`${failed.length} products failed during synchronization`);
}
res.json({ synced: results.length - failed.length, failed: failed.length });
});
The key to this method is using **product SKUs** as the common identifier between Odoo and Shopify. The SKU (internal reference in Odoo, `default_code`) must be the same in both systems. Before implementing any integration, audit your catalogue and make sure every product has a unique, consistent SKU in both systems. This preparatory step prevents 80% of synchronization problems.
Method 2: Official connectors and the Odoo marketplace
There are modules on the Odoo Apps marketplace (apps.odoo.com) that offer pre-configured connectors for Shopify. The most popular ones include:
- Shopify Odoo Connector (vendors such as Emipro, Vraja Technologies): modules that add an interface inside Odoo to configure synchronization without writing code.
- Shopify Integration by OdooTec: a popular solution with multi-store support.
Pros of marketplace connectors
- Reduced implementation time: Installation and configuration in hours, not weeks.
- Included maintenance: The vendor updates the connector with each new Odoo version and Shopify API changes.
- Visual interface: No need for the technical team to manage integration code.
- Covered scenarios: Most standard flows (orders, stock, customers, prices) are handled out of the box.
Cons of marketplace connectors
- Recurring cost: Between €150 and €600/year per licence, plus support.
- Low flexibility: If your business logic is specific (complex pricing rules, multiple warehouses, custom shipping logic), standard connectors often fall short.
- Third-party dependency: If the vendor stops maintaining the module, you have a problem.
- Conflicts with other customizations: Third-party modules sometimes clash with each other or with your own custom modules.
Marketplace connectors are ideal for stores with standard flows and moderate volume (up to ~500 orders/day). For more complex cases, a custom integration is usually a better long-term investment.
Method 3: Automation middleware vs. custom code
Make (formerly Integromat) and Zapier
Tools like Make or Zapier allow you to connect Shopify and Odoo without code using visual flows. They have native connectors for both systems.
When to use Make/Zapier:
- Low to medium volume (fewer than 200 orders/day).
- The team does not have development resources.
- Flows are simple and standard (new order → create in Odoo, no complex logic).
- Validation phase before investing in a custom integration.
When NOT to use Make/Zapier:
- More than 500 orders/day (execution costs scale quickly).
- You need complex logic: customer deduplication, data transformations, sophisticated error handling.
- You have low-latency requirements (real-time stock synchronization).
- You need complete traceability and audit of every operation.
Custom code
A purpose-built integration service (Python + FastAPI, Node.js + Express) gives you full control. It is more expensive to develop initially but cheaper in the long run for high volumes, and far more flexible.
# Example: more sophisticated customer deduplication logic
# that a standard connector typically does not include
def find_or_create_partner(models, uid, shopify_customer):
"""
Deduplication strategy by priority:
1. Search by email (most reliable)
2. Search by phone if no email
3. Search by name + address if no email or phone
4. Create if no match found
"""
email = shopify_customer.get('email', '').lower().strip()
phone = shopify_customer.get('phone', '').strip()
# Step 1: search by email
if email:
ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'search',
[[['email', '=ilike', email], ['active', 'in', [True, False]]]]
)
if ids:
return ids[0], 'found_by_email'
# Step 2: search by phone
if phone:
normalized_phone = ''.join(filter(str.isdigit, phone))[-9:]
ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'search',
[[['phone', 'like', normalized_phone]]]
)
if ids:
return ids[0], 'found_by_phone'
# Step 3: create new partner
name = f"{shopify_customer.get('first_name', '')} {shopify_customer.get('last_name', '')}".strip()
new_id = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'create',
[{
'name': name or 'Unnamed customer',
'email': email,
'phone': phone,
'customer_rank': 1,
}]
)
return new_id, 'created'
Common mistakes and how to avoid them
Duplicate or negative stock
Problem: If Shopify and Odoo adjust stock independently without coordination, you can end up with stock that does not exist or with sales of out-of-stock products.
Solution: Establish a single source of truth. In most cases Odoo is the master inventory system and Shopify only reflects what Odoo says. Never adjust stock directly in Shopify when the integration is active.
Duplicate orders
Problem: A webhook can arrive twice (Shopify guarantees at-least-once delivery). If you do not handle idempotency, you create the order twice in Odoo.
Solution: Store the Shopify order_id in Odoo (in the client_order_ref field or in a custom field) and check before creating:
def is_order_already_imported(models, uid, shopify_order_id):
"""Checks whether a Shopify order has already been imported into Odoo."""
existing = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'sale.order', 'search_count',
[[['client_order_ref', '=', f'shopify_{shopify_order_id}']]]
)
return existing > 0
Desynchronized product IDs
Problem: Shopify uses variant_id and product_id. Odoo uses product.product (variant) and product.template (base product). The mapping is not trivial, especially for products with multiple variants (size, colour).
Solution: Maintain an explicit mapping table. This can be as simple as a custom field on product.product in Odoo that stores the shopify_variant_id:
# Add shopify_variant_id field to the product.product model in Odoo
# In a custom module (models/product.py):
from odoo import models, fields
class ProductProduct(models.Model):
_inherit = 'product.product'
shopify_variant_id = fields.Char(
string='Shopify Variant ID',
index=True,
copy=False,
)
shopify_product_id = fields.Char(
string='Shopify Product ID',
index=True,
copy=False,
)
Silent webhook failures
Problem: A Shopify webhook fails (timeout, 500 error) and the order never reaches Odoo. Shopify retries for 48 hours but if the problem persists, it is discarded.
Solution: Implement a compensation process: a cron job that every hour queries Shopify orders from the last two hours and verifies they all exist in Odoo. If any are missing, it imports them.
// Reconciliation process: cron every hour
async function reconcileRecentOrders() {
const oneHourAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
// Get recent orders from Shopify
const { data } = await shopifyClient.get('/orders.json', {
params: {
created_at_min: oneHourAgo,
status: 'any',
limit: 250,
},
});
for (const order of data.orders) {
const exists = await checkOrderInOdoo(order.id);
if (!exists) {
console.warn(`Order ${order.name} not found in Odoo. Re-importing...`);
await importOrderToOdoo(order);
}
}
}
Recommended architecture for stores with more than 1,000 orders/day
At high volume, the architecture must be resilient to traffic spikes, partial failures and network delays:
[Shopify Webhooks]
|
v
[Message queue: Redis / RabbitMQ]
|
v
[Processing workers (multiple instances)]
|
+---> [Odoo XML-RPC / REST API]
|
+---> [Audit database]
|
v
[Reconciliation cron (every 1h)]
|
v
[Alerts: Slack / PagerDuty on discrepancies]
Key principles:
- Message queue: Shopify webhooks write to a queue (Redis Streams, RabbitMQ). Workers process asynchronously. This prevents an order spike from overwhelming Odoo.
- Scalable workers: Multiple worker processes consuming the queue in parallel. They scale horizontally with the load.
- Guaranteed idempotency: Every operation has an
idempotency_keybased on the Shopify event ID. If a worker processes the same event twice, the result is the same. - Full audit trail: Every operation (webhook received, order created, stock updated) is logged to a database with timestamp, payload and result. Essential for debugging.
- Circuit breaker: If Odoo does not respond for more than N seconds, the worker stops retrying and places messages in a retry queue. This prevents failure cascades.
Implementation checklist
Before going live with the integration, verify each item:
Catalogue preparation
- All products have a unique SKU in Odoo (
default_code) - Odoo SKUs match Shopify SKUs/references
- Products with variants (size, colour) are correctly mapped variant to variant
- The
shopify_variant_id ↔ odoo_product_idmapping table is complete
Technical setup
- Shopify webhooks configured for:
orders/create,orders/updated,orders/cancelled,refunds/create - HMAC signature verification active on all webhook endpoints
- Environment variables configured (never credentials in code)
- Structured logs enabled with INFO or DEBUG level during testing
Data flows
- New Shopify order → sales order in Odoo (tested with real orders)
- Cancellation in Shopify → cancellation in Odoo
- Return in Shopify → return/credit note in Odoo
- Stock change in Odoo → update in Shopify (tested with manual inventory adjustment)
- New customer in Shopify → partner in Odoo (no duplicates)
Resilience
- Idempotency verified (processing the same webhook twice does not create duplicates)
- Periodic reconciliation process active
- Alerts configured for synchronization failures
- Recovery plan for Odoo downtime (message queue holds data without loss)
Load testing
- Simulated order spike (using Shopify development store + test data)
- Verified that order processing time is under 30 seconds under normal conditions
How Soamee can help
At Soamee we specialize in ERP and e-commerce integrations. We have implemented Odoo–Shopify integrations for clients with very different volumes, from growing stores to operations handling thousands of daily orders.
Our approach always starts by understanding your specific case: which data you need to synchronize, how frequently, what particular business logic you have (multiple warehouses, different rates per channel, bundle management, partial dropshipping). Only then do we recommend the appropriate method, whether a standard connector, a custom middleware or an event-driven architecture.
If you are considering integrating Odoo with Shopify, or if you have an existing integration that is not working well, book a free consultation. In 45 minutes we can give you a clear roadmap with options, effort estimates and a technical recommendation.
Conclusion
The integration between Odoo and Shopify is not a question of “if” but of “how”. The right method depends on your volume, the complexity of your business logic and your technical resources:
- Marketplace connectors: Ideal for getting started quickly with standard flows and moderate volume.
- Make/Zapier: For validating the need without development investment, at low volume.
- Custom API integration: The most robust and flexible option for operations that scale, with proprietary logic and real resilience.
Whatever method you choose, the pillars are the same: SKU as the mapping key, idempotency in all flows, a single source of truth for stock, and a reconciliation process that detects and corrects discrepancies before they become customer-facing problems.