Shopify + Odoo ERP Integration: The Complete Guide

Comprehensive guide to integrating Shopify with Odoo ERP — inventory sync, order management, customer data, financial reporting, and automation workflows.

E
ECOSIRE Research and Development Team
|March 19, 202611 min read2.3k Words|

Part of our eCommerce Integration series

Read the complete guide

Shopify + Odoo ERP Integration: The Complete Guide

As Shopify stores scale, the gap between what Shopify handles natively and what the business actually needs for operational excellence becomes a significant constraint. Inventory management across multiple warehouses, multi-currency accounting, manufacturing orders triggered by Shopify sales, sophisticated CRM with full customer history, and automated vendor replenishment — these require an ERP, and Odoo is increasingly the system of choice for mid-market ecommerce businesses.

Shopify + Odoo is not a plug-and-play integration. It requires careful architectural decisions about which system owns which data, what syncs in which direction, and how to handle edge cases like partial shipments, return processing, and variant mapping. This guide covers everything from integration architecture to specific implementation patterns.

Key Takeaways

  • Odoo is the source of truth for inventory, customers, and accounting; Shopify is the source of truth for ecommerce transactions
  • Bi-directional inventory sync (Shopify → Odoo for orders, Odoo → Shopify for stock levels) is the core integration requirement
  • Product catalog management should live in Odoo and sync to Shopify — not the reverse
  • Customer records merge between systems using email as the unique identifier
  • Order lifecycle: Shopify creates order → Odoo receives order → Odoo creates delivery → fulfillment updates Shopify
  • Return processing requires coordination: Shopify initiates return → Odoo processes receipt → both systems update
  • Shopify's Webhook API provides real-time order events; Odoo receives these via connector middleware
  • Direct Odoo-Shopify connectors exist (Syncee, OdooConnector) but custom integrations via REST API offer more control

Understanding the Integration Architecture

Before implementing, define the authoritative system for each data domain:

Data TypeAuthoritative SystemSync DirectionFrequency
Product catalogOdooOdoo → ShopifyOn product change
Inventory levelsOdooOdoo → ShopifyReal-time
OrdersShopifyShopify → OdooReal-time (webhook)
Customer recordsOdoo (merged)Bidirectional (email key)On order
PricingOdooOdoo → ShopifyOn price change
Shipping ratesShopifyShopify onlyN/A
PaymentsShopifyShopify → Odoo (accounting)On settlement
ReturnsShopify initiatedShopify → OdooOn return creation

Why Odoo owns inventory:

Shopify's inventory tracking is functional but limited for multi-warehouse, multi-channel operations. Odoo's inventory module handles: lot and serial number tracking, multi-location warehouse management, automated replenishment rules, manufacturing integration (finished goods decrement component inventory), and barcode-driven fulfillment operations. Shopify must reflect Odoo's inventory reality, not the other way around.

Why Shopify is the commerce layer:

Shopify's checkout, payment processing, shipping rate calculation, tax collection, and customer-facing experience are best-in-class. Odoo's B2C ecommerce (Odoo Website) is functional but not at Shopify's level for DTC commerce. The optimal architecture keeps Shopify as the commerce interface and Odoo as the operational backbone.


Integration Methods: Connector Apps vs. Custom API

Option 1: Pre-built connector apps

Several Shopify and Odoo marketplace apps provide pre-built integration:

ConnectorApproachMonthly CostProsCons
Zapiet + OdooMiddleware via Zapier$50-200Quick setupLimited customization, single-point failure
SynceeDirect connector$29-99Catalog syncOrder processing limited
OdooConnector.comPurpose-built$200-500ComprehensiveRequires Odoo expertise
Eshop+ (Odoo App)Odoo-nativeCommunity freeOdoo-nativeBasic Shopify support
Webkul Shopify OdooPurpose-builtCustomFull lifecycleComplex configuration

Pre-built connectors work well for: standard product catalog and order sync for simple business models without complex variant structures, multiple warehouses, or manufacturing dependencies.

Option 2: Custom integration via APIs

For complex business requirements, a custom integration using Shopify's REST/GraphQL API and Odoo's JSON-RPC/REST API provides the most control and reliability.

Custom integration architecture:

Shopify (Commerce Layer)
│
│ Webhooks (orders/create, orders/updated, refunds/create, inventory_levels/update)
│
▼
Integration Service (Node.js / Python middleware)
│
│ Event processing, transformation, error handling, retry logic
│
▼
Odoo (ERP Layer)
│
│ Odoo JSON-RPC API (sales orders, inventory, customers, accounting)
│
└── Inventory updates → Shopify Admin API

Technology stack for custom integration:

  • Middleware: Node.js (for Shopify ecosystem alignment) or Python (for Odoo ecosystem alignment)
  • Queue: Redis or RabbitMQ for reliable event processing
  • Database: PostgreSQL for integration state, idempotency keys, error logs
  • Hosting: AWS Lambda or similar for webhook handlers (scales automatically with Shopify traffic spikes)

Order Sync: Shopify → Odoo

The order sync is the most critical integration path. Every Shopify order must create a corresponding Odoo sale order that triggers fulfillment and updates financial records.

Shopify webhook setup for order events:

// Register webhooks via Shopify API
const webhooks = [
  {
    topic: 'orders/create',
    address: 'https://your-integration.com/webhooks/shopify/orders',
    format: 'json'
  },
  {
    topic: 'orders/updated',
    address: 'https://your-integration.com/webhooks/shopify/orders/updated',
    format: 'json'
  },
  {
    topic: 'orders/fulfilled',
    address: 'https://your-integration.com/webhooks/shopify/orders/fulfilled',
    format: 'json'
  },
  {
    topic: 'refunds/create',
    address: 'https://your-integration.com/webhooks/shopify/refunds',
    format: 'json'
  }
];

Transforming a Shopify order to an Odoo sale order:

def shopify_order_to_odoo_sale_order(shopify_order: dict) -> dict:
    """Transform Shopify order payload to Odoo sale.order format"""

    # Find or create Odoo partner (customer)
    partner_id = find_or_create_odoo_partner(
        email=shopify_order['email'],
        name=shopify_order['customer']['first_name'] + ' ' + shopify_order['customer']['last_name'],
        phone=shopify_order['customer'].get('phone'),
        shipping_address=shopify_order['shipping_address']
    )

    # Map line items
    order_lines = []
    for item in shopify_order['line_items']:
        odoo_product_id = get_odoo_product_from_shopify_variant(
            item['variant_id']
        )
        order_lines.append({
            'product_id': odoo_product_id,
            'product_uom_qty': item['quantity'],
            'price_unit': float(item['price']),
            'name': item['name'],
            'shopify_line_id': item['id'],  # Custom field for traceability
        })

    # Add shipping as a service product line
    if float(shopify_order.get('shipping_lines', [{}])[0].get('price', 0)) > 0:
        order_lines.append({
            'product_id': SHIPPING_PRODUCT_ID,  # Configured in settings
            'product_uom_qty': 1,
            'price_unit': float(shopify_order['shipping_lines'][0]['price']),
            'name': shopify_order['shipping_lines'][0]['title'],
        })

    return {
        'partner_id': partner_id,
        'order_line': [(0, 0, line) for line in order_lines],
        'shopify_order_id': shopify_order['id'],  # Custom field
        'shopify_order_name': shopify_order['name'],  # e.g., #1001
        'note': shopify_order.get('note', ''),
        'state': 'sale',  # Confirm order automatically
    }

Idempotency handling:

Shopify may deliver the same webhook event multiple times (network retries). Your integration must handle duplicate events gracefully:

def process_shopify_order_webhook(payload: dict):
    shopify_order_id = str(payload['id'])

    # Check if already processed
    if OrderSyncLog.objects.filter(
        shopify_order_id=shopify_order_id,
        status='completed'
    ).exists():
        logger.info(f"Order {shopify_order_id} already processed, skipping")
        return

    # Process and log
    try:
        odoo_order_id = create_odoo_sale_order(payload)
        OrderSyncLog.objects.create(
            shopify_order_id=shopify_order_id,
            odoo_order_id=odoo_order_id,
            status='completed'
        )
    except Exception as e:
        OrderSyncLog.objects.create(
            shopify_order_id=shopify_order_id,
            status='failed',
            error=str(e)
        )
        raise

Inventory Sync: Odoo → Shopify

Inventory levels must reflect Odoo's reality in Shopify in real-time (or near real-time) to prevent overselling.

Trigger-based inventory sync:

The most reliable approach is event-driven sync: when inventory changes in Odoo (sale, purchase receipt, manufacturing completion, stock adjustment), Odoo pushes the updated quantity to Shopify.

# In Odoo (using automated actions or override)
def _post_write_sync_to_shopify(self):
    """Called after inventory level changes in Odoo"""
    for move_line in self:
        product = move_line.product_id
        location = move_line.location_id

        if location.is_shopify_sync_location:
            shopify_variant_id = product.shopify_variant_id
            if shopify_variant_id:
                new_quantity = product.with_context(
                    location=location.id
                ).qty_available

                sync_inventory_to_shopify(
                    shopify_variant_id=shopify_variant_id,
                    quantity=int(new_quantity)
                )

def sync_inventory_to_shopify(shopify_variant_id: str, quantity: int):
    """Push inventory update to Shopify via Admin API"""
    inventory_item_id = get_inventory_item_id(shopify_variant_id)
    location_id = get_shopify_location_id()  # Primary Shopify location

    shopify.InventoryLevel.set(
        inventory_item_id=inventory_item_id,
        location_id=location_id,
        available=quantity
    )

Scheduled inventory reconciliation:

Even with event-driven sync, schedule a daily full inventory reconciliation:

  1. Export all Odoo product quantities from the designated Shopify-sync location
  2. Compare against current Shopify inventory levels
  3. Update any discrepancies (may occur due to failed sync events, manual adjustments)
  4. Log reconciliation results for audit purposes

This reconciliation prevents inventory drift from accumulated small sync failures.


Product Catalog Sync: Odoo → Shopify

For businesses managing product catalogs in Odoo (with multi-currency pricing, detailed specifications, and complex variant matrices), syncing the catalog to Shopify eliminates manual double-entry.

Product mapping architecture:

Odoo Product (product.template)
├── Shopify Product (via shopify_product_id field on Odoo template)
│
└── Odoo Product Variants (product.product)
    └── Shopify Variants (via shopify_variant_id field on Odoo product.product)

What to sync from Odoo to Shopify:

  • Product name (Odoo's sales description)
  • Product description (long HTML description)
  • Images (product.template images)
  • Price (using the configured Shopify pricelist)
  • SKU (Odoo's internal reference)
  • Barcode (EAN/UPC from Odoo)
  • Weight (for shipping calculation)
  • Active/archived status (unpublish in Shopify when Odoo product is archived)
  • Inventory (from the designated sync location)

What NOT to sync from Odoo to Shopify:

  • Shopify-specific SEO metadata (title tags, meta descriptions — manage in Shopify)
  • Shopify product tags (manage in Shopify)
  • Shopify collections/categories (manage in Shopify)
  • Shopify-specific content (page builder sections, rich descriptions formatted for Shopify)

Customer Data Management

Customers who exist in both Shopify (from their storefront account) and Odoo (as contacts/partners) need careful merging to create a single unified profile.

Deduplication strategy using email:

def find_or_create_odoo_partner(email: str, name: str, **kwargs) -> int:
    """Find existing Odoo partner by email or create new one"""
    existing = Partner.search([
        ('email', '=', email)
    ], limit=1)

    if existing:
        # Update with latest data from Shopify
        existing.write({
            'phone': kwargs.get('phone', existing.phone),
        })
        return existing.id
    else:
        # Create new partner
        partner = Partner.create({
            'name': name,
            'email': email,
            'phone': kwargs.get('phone'),
            'type': 'contact',
            'customer_rank': 1,
            'shopify_customer_id': kwargs.get('shopify_customer_id'),
        })
        return partner.id

Shopify customer IDs stored in Odoo:

Add a custom field shopify_customer_id to Odoo's res.partner model. This enables bidirectional lookup: find Odoo partner from Shopify ID, find Shopify customer from Odoo partner.


Fulfillment Loop: Odoo → Shopify

When Odoo processes a delivery (picking + validate), the order is fulfilled. Shopify must be notified to:

  • Mark the order as fulfilled
  • Send the shipping confirmation email to the customer
  • Record the tracking number
def sync_fulfillment_to_shopify(odoo_picking: StockPicking):
    """Called after Odoo delivery is validated"""
    shopify_order_name = odoo_picking.sale_id.shopify_order_name
    tracking_number = odoo_picking.carrier_tracking_ref

    # Find Shopify order
    shopify_orders = shopify.Order.find(name=shopify_order_name)
    if not shopify_orders:
        return

    shopify_order = shopify_orders[0]

    # Create fulfillment in Shopify
    fulfillment = shopify.Fulfillment.create({
        'order_id': shopify_order.id,
        'tracking_number': tracking_number,
        'tracking_company': odoo_picking.carrier_id.name,
        'notify_customer': True,  # Sends Shopify's shipping email
        'line_items': [
            {'id': line.shopify_line_id}
            for line in odoo_picking.sale_id.order_line
            if line.shopify_line_id
        ]
    })

Accounting Integration: Shopify Sales → Odoo Financials

Every Shopify sale must eventually appear in Odoo's accounting module as a posted sale entry.

Integration approach for accounting:

Option 1 — Order-level accounting: Each Shopify order creates an Odoo invoice (or the Odoo sale order generates an invoice when fulfilled). Payments recorded in Shopify trigger payment registration in Odoo.

Option 2 — Settlement-level accounting: Shopify Payments settlements (daily or weekly bank deposits) are recorded in Odoo as journal entries that reconcile against bank transactions. This is simpler to maintain but provides less granular accounting.

For most mid-market merchants, settlement-level accounting (Option 2) is sufficient and significantly less complex to implement and maintain.

Shopify payout data → Odoo journal entry:

def process_shopify_payout(payout_data: dict):
    """Create Odoo journal entry for Shopify Payments payout"""
    journal = ShopifyJournal.get_or_create()  # Shopify clearing account

    entry = AccountMove.create({
        'journal_id': journal.id,
        'date': payout_data['date'],
        'ref': f"Shopify Payout {payout_data['id']}",
        'line_ids': [
            (0, 0, {
                'account_id': SHOPIFY_CLEARING_ACCOUNT_ID,
                'credit': payout_data['amount'],
                'name': f"Shopify sales - {payout_data['period']}",
            }),
            (0, 0, {
                'account_id': SHOPIFY_FEES_ACCOUNT_ID,
                'debit': payout_data['fees'],
                'name': 'Shopify Payments fees',
            }),
            (0, 0, {
                'account_id': BANK_ACCOUNT_ID,
                'debit': payout_data['amount'] - payout_data['fees'],
                'name': f"Bank deposit - Shopify payout {payout_data['id']}",
            }),
        ]
    })
    entry.action_post()

Frequently Asked Questions

How long does a Shopify-Odoo integration take to implement?

A basic integration (order sync, inventory sync, customer sync) using a pre-built connector takes 2-4 weeks including configuration, testing, and data migration. A custom integration covering the full lifecycle (orders, inventory, fulfillment sync, returns, accounting) takes 8-16 weeks depending on business complexity. Complex scenarios — multi-warehouse, manufacturing, multi-currency, multi-company Odoo — add 4-8 additional weeks. Budget for ongoing maintenance: integrations require updates when Shopify or Odoo release API changes.

Should I manage products in Shopify or Odoo?

For simple product catalogs: manage in Shopify and manually update Odoo for manufacturing/procurement purposes. For complex catalogs (many variants, multi-currency pricing, technical specifications, manufacturing BOMs): manage in Odoo and sync to Shopify. The critical factor is where your product team actually works. If your merchandising team lives in Shopify, forcing them to work in Odoo creates friction. If your operations team manages products in Odoo for manufacturing and procurement, Shopify sync is the right approach.

What happens to existing Shopify orders when the integration goes live?

Historical orders do not need to migrate to Odoo. The integration processes new orders from the go-live date forward. For historical data (customer records, product catalog, inventory baselines), perform a one-time data migration before the integration goes live: import historical customer data to Odoo, import product catalog, and set inventory baselines in Odoo to match the current Shopify quantities.

How do I handle Shopify orders with products that don't exist in Odoo?

This edge case breaks naive integrations. Build a fallback: when a Shopify order contains a variant ID that does not map to an Odoo product, create the order in Odoo with a placeholder "Unknown Product" product and alert your integration team. Define an error queue with notification: operations staff review unmapped products, create the Odoo product, and reprocess the failed order. This is preferable to silent failure or blocking all orders while awaiting a mapping fix.

Can this integration support multiple Shopify stores (e.g., different market stores)?

Yes, but with added complexity. Each Shopify store is a separate API connection. One Odoo instance can receive orders from multiple Shopify stores, with the store source tracked via a custom field. Inventory allocation between stores requires additional logic: either shared inventory pool (Odoo allocates across stores based on order demand) or location-segregated inventory (each store has a designated Odoo location). Multi-store integration doubles the testing scope and ongoing maintenance burden.


Next Steps

A well-implemented Shopify-Odoo integration transforms operational efficiency: eliminating manual data entry, preventing overselling, enabling sophisticated reporting, and connecting ecommerce sales to manufacturing, procurement, and financial processes.

ECOSIRE builds Shopify and Odoo ERP integrations for mid-market merchants — covering architecture design, custom development, data migration, testing, and ongoing support. Our integration team has implemented Shopify-Odoo connections for 30+ merchants across diverse product categories.

Contact our integration team to design your Shopify-Odoo integration architecture.

E

Written by

ECOSIRE Research and Development Team

Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.

Chat on WhatsApp