Retour au blog

Shopify + CRM : Intégration webhooks réussie

28 Août 2024 6 min de lecture E-commerce

Guide technique complet de l'intégration Shopify-CRM que j'ai développée. Webhooks JSON, synchronisation temps réel et gestion des erreurs. Code Python inclus.

Le projet : Synchronisation automatique

Client : Boutique e-commerce (Marseille)

Problème : Saisie manuelle des commandes Shopify dans le CRM

Solution : Webhooks automatiques + API REST

Architecture de l'intégration

# Structure du projet
shopify-crm-integration/
├── app.py                 # Application Flask principale
├── webhooks.py           # Gestion des webhooks Shopify
├── crm_client.py         # Client API pour le CRM
├── models.py             # Modèles de données
├── config.py             # Configuration
└── requirements.txt      # Dépendances Python

Article Premium

Cet article contient du code Python complet pour l'intégration Shopify-CRM, des techniques de webhooks et de gestion d'erreurs.

Ce que vous découvrirez :

  • Code Python complet pour webhooks
  • Gestion des erreurs et retry logic
  • Intégration API REST
  • Monitoring et logs avancés
  • Sécurité et validation HMAC
  • Architecture modulaire

Configuration des webhooks Shopify

# Configuration des webhooks dans Shopify Admin
webhooks = [
    {
        "topic": "orders/create",
        "address": "https://votre-domaine.com/webhook/order-created",
        "format": "json"
    },
    {
        "topic": "orders/updated", 
        "address": "https://votre-domaine.com/webhook/order-updated",
        "format": "json"
    },
    {
        "topic": "orders/paid",
        "address": "https://votre-domaine.com/webhook/order-paid", 
        "format": "json"
    }
]

Gestionnaire de webhooks Flask

from flask import Flask, request, jsonify
import hmac
import hashlib
import json
from crm_client import CRMClient

app = Flask(__name__)

@app.route('/webhook/order-created', methods=['POST'])
def handle_order_created():
    # Vérification de l'authenticité du webhook
    if not verify_webhook(request):
        return jsonify({'error': 'Unauthorized'}), 401
    
    try:
        order_data = request.get_json()
        
        # Traitement de la commande
        processed_order = process_shopify_order(order_data)
        
        # Envoi vers le CRM
        crm_client = CRMClient()
        result = crm_client.create_order(processed_order)
        
        if result['success']:
            return jsonify({'status': 'success', 'crm_id': result['id']})
        else:
            return jsonify({'error': result['error']}), 500
            
    except Exception as e:
        return jsonify({'error': str(e)}), 500

def verify_webhook(request):
    """Vérification de l'authenticité du webhook Shopify"""
    hmac_header = request.headers.get('X-Shopify-Hmac-Sha256')
    calculated_hmac = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        request.get_data(),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(calculated_hmac, hmac_header)

Transformation des données Shopify

def process_shopify_order(shopify_order):
    """Transforme une commande Shopify en format CRM"""
    
    # Extraction des informations client
    customer = shopify_order.get('customer', {})
    billing_address = shopify_order.get('billing_address', {})
    
    # Calcul du total
    total_price = float(shopify_order['total_price'])
    tax_amount = float(shopify_order.get('total_tax', 0))
    
    # Formatage pour le CRM
    crm_order = {
        'external_id': shopify_order['id'],
        'order_number': shopify_order['name'],
        'customer': {
            'email': customer.get('email'),
            'first_name': customer.get('first_name'),
            'last_name': customer.get('last_name'),
            'phone': customer.get('phone'),
            'address': {
                'street': billing_address.get('address1'),
                'city': billing_address.get('city'),
                'zip_code': billing_address.get('zip'),
                'country': billing_address.get('country')
            }
        },
        'items': [],
        'totals': {
            'subtotal': total_price - tax_amount,
            'tax': tax_amount,
            'total': total_price
        },
        'status': map_order_status(shopify_order['fulfillment_status']),
        'created_at': shopify_order['created_at']
    }
    
    # Traitement des articles
    for line_item in shopify_order.get('line_items', []):
        crm_order['items'].append({
            'product_id': line_item['product_id'],
            'variant_id': line_item['variant_id'],
            'title': line_item['title'],
            'quantity': line_item['quantity'],
            'price': float(line_item['price'])
        })
    
    return crm_order

def map_order_status(shopify_status):
    """Mappe les statuts Shopify vers le CRM"""
    status_mapping = {
        None: 'pending',
        'fulfilled': 'completed',
        'partial': 'processing',
        'restocked': 'cancelled'
    }
    return status_mapping.get(shopify_status, 'pending')

Client API pour le CRM

import requests
import json
from datetime import datetime

class CRMClient:
    def __init__(self):
        self.base_url = CRM_API_URL
        self.api_key = CRM_API_KEY
        self.headers = {
            'Authorization': f'Bearer {self.api_key}',
            'Content-Type': 'application/json'
        }
    
    def create_order(self, order_data):
        """Crée une commande dans le CRM"""
        try:
            response = requests.post(
                f"{self.base_url}/api/orders",
                headers=self.headers,
                json=order_data,
                timeout=30
            )
            
            if response.status_code == 201:
                return {
                    'success': True,
                    'id': response.json().get('id'),
                    'message': 'Commande créée avec succès'
                }
            else:
                return {
                    'success': False,
                    'error': f'Erreur CRM: {response.status_code} - {response.text}'
                }
                
        except requests.exceptions.Timeout:
            return {
                'success': False,
                'error': 'Timeout - Le CRM ne répond pas'
            }
        except Exception as e:
            return {
                'success': False,
                'error': f'Erreur de connexion: {str(e)}'
            }
    
    def update_order(self, order_id, update_data):
        """Met à jour une commande existante"""
        try:
            response = requests.put(
                f"{self.base_url}/api/orders/{order_id}",
                headers=self.headers,
                json=update_data,
                timeout=30
            )
            
            return response.status_code == 200
            
        except Exception as e:
            print(f"Erreur mise à jour commande: {e}")
            return False

Gestion des erreurs et retry

import time
from functools import wraps

def retry_on_failure(max_retries=3, delay=1):
    """Décorateur pour retry automatique"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    result = func(*args, **kwargs)
                    if result.get('success'):
                        return result
                except Exception as e:
                    if attempt == max_retries - 1:
                        return {
                            'success': False,
                            'error': f'Échec après {max_retries} tentatives: {str(e)}'
                        }
                    time.sleep(delay * (2 ** attempt))  # Backoff exponentiel
            return {'success': False, 'error': 'Max retries atteint'}
        return wrapper
    return decorator

@retry_on_failure(max_retries=3, delay=2)
def sync_order_to_crm(order_data):
    """Synchronise une commande avec retry automatique"""
    crm_client = CRMClient()
    return crm_client.create_order(order_data)

Monitoring et logs

import logging
from datetime import datetime

# Configuration des logs
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('shopify_crm.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

def log_webhook_event(event_type, order_id, status, details=None):
    """Log des événements webhook"""
    log_data = {
        'timestamp': datetime.now().isoformat(),
        'event_type': event_type,
        'order_id': order_id,
        'status': status,
        'details': details
    }
    
    logger.info(f"Webhook {event_type}: {json.dumps(log_data)}")

# Utilisation dans les webhooks
@app.route('/webhook/order-created', methods=['POST'])
def handle_order_created():
    order_data = request.get_json()
    order_id = order_data.get('id')
    
    try:
        # ... traitement ...
        log_webhook_event('order_created', order_id, 'success')
        return jsonify({'status': 'success'})
    except Exception as e:
        log_webhook_event('order_created', order_id, 'error', str(e))
        return jsonify({'error': str(e)}), 500

Résultats obtenus

Points techniques importants

  1. Sécurité : Vérification HMAC des webhooks
  2. Idempotence : Éviter les doublons avec external_id
  3. Retry logic : Gestion des pannes temporaires
  4. Monitoring : Logs et alertes en cas d'erreur

🚀 Besoin d'intégrer vos outils e-commerce ?

Chaque intégration a ses spécificités. Contactez-moi pour une solution sur-mesure adaptée à votre stack technique.

Discuter de votre projet