> ## Documentation Index
> Fetch the complete documentation index at: https://quickbutik.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Order Synchronization Tutorial

> Build a complete order sync system with webhooks and automatic retry logic

This tutorial shows you how to build a robust order synchronization system that automatically syncs orders from Quickbutik to your external system using webhooks and the API.

<Info>
  **What you'll learn:**

  * Set up webhook endpoints to receive order notifications
  * Implement automatic retry logic for failed API calls
  * Handle order status updates bidirectionally
  * Build a production-ready integration with proper error handling
</Info>

## Architecture Overview

```mermaid theme={null}
graph LR
    A[Quickbutik Store] -->|Webhook| B[Your Webhook Endpoint]
    B --> C[Order Processing Service]
    C --> D[External System]
    C -->|Status Updates| E[Quickbutik API]
    C --> F[Database/Queue]
```

## Part 1: Setting Up Webhook Endpoint

First, let's create a webhook endpoint to receive order notifications:

<CodeGroup>
  ```javascript Express.js theme={null}
  const express = require('express');
  const crypto = require('crypto');
  const app = express();

  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));

  // Webhook endpoint for order notifications
  app.post('/webhooks/quickbutik', async (req, res) => {
    try {
      const { event_type, order_id } = req.query;
      
      console.log(`Received webhook: ${event_type} for order ${order_id}`);
      
      // Acknowledge webhook immediately
      res.status(200).send('OK');
      
      // Process webhook asynchronously
      await processOrderWebhook(event_type, order_id);
      
    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(500).send('Error processing webhook');
    }
  });

  async function processOrderWebhook(eventType, orderId) {
    switch (eventType) {
      case 'order.new':
        await handleNewOrder(orderId);
        break;
      case 'order.done':
        await handleOrderShipped(orderId);
        break;
      case 'order.cancelled':
        await handleOrderCancelled(orderId);
        break;
      default:
        console.log(`Unhandled event type: ${eventType}`);
    }
  }

  app.listen(3000, () => {
    console.log('Webhook server running on port 3000');
  });
  ```

  ```python Flask theme={null}
  from flask import Flask, request, jsonify
  import asyncio
  import logging

  app = Flask(__name__)
  logging.basicConfig(level=logging.INFO)

  @app.route('/webhooks/quickbutik', methods=['POST'])
  def quickbutik_webhook():
      try:
          event_type = request.args.get('event_type')
          order_id = request.args.get('order_id')
          
          app.logger.info(f'Received webhook: {event_type} for order {order_id}')
          
          # Acknowledge webhook immediately
          response = jsonify({'status': 'received'})
          
          # Process webhook asynchronously (in production, use a task queue)
          asyncio.create_task(process_order_webhook(event_type, order_id))
          
          return response, 200
          
      except Exception as error:
          app.logger.error(f'Webhook processing error: {error}')
          return jsonify({'error': 'Error processing webhook'}), 500

  async def process_order_webhook(event_type, order_id):
      try:
          if event_type == 'order.new':
              await handle_new_order(order_id)
          elif event_type == 'order.done':
              await handle_order_shipped(order_id)
          elif event_type == 'order.cancelled':
              await handle_order_cancelled(order_id)
          else:
              app.logger.info(f'Unhandled event type: {event_type}')
      except Exception as error:
          app.logger.error(f'Error processing webhook: {error}')

  if __name__ == '__main__':
      app.run(debug=True, port=3000)
  ```
</CodeGroup>

<Warning>
  **Important:** Always acknowledge webhooks quickly (within 10 seconds) to avoid retries. Process the actual work asynchronously.
</Warning>

## Part 2: Fetching Order Details

When you receive a webhook, fetch the complete order details:

<CodeGroup>
  ```javascript Node.js theme={null}
  const QuickbutikAPI = require('./quickbutik-api'); // From our previous tutorial

  class OrderSyncService {
    constructor(apiKey) {
      this.api = new QuickbutikAPI(apiKey);
      this.retryAttempts = 3;
      this.retryDelay = 1000; // 1 second
    }

    async handleNewOrder(orderId) {
      try {
        // Fetch complete order details
        const orders = await this.retryAPICall(() => 
          this.api.getOrders({ 
            order_id: orderId, 
            include_details: true,
            apps_load: true 
          })
        );

        if (!orders || orders.length === 0) {
          throw new Error(`Order ${orderId} not found`);
        }

        const order = orders[0];
        
        // Validate order data
        if (!this.validateOrder(order)) {
          throw new Error(`Invalid order data for order ${orderId}`);
        }

        // Sync to external system
        await this.syncOrderToExternalSystem(order);
        
        // Log success
        console.log(`Successfully synced order ${orderId} to external system`);

      } catch (error) {
        console.error(`Failed to process new order ${orderId}:`, error);
        
        // Add to retry queue or send alert
        await this.handleOrderSyncFailure(orderId, error);
      }
    }

    async retryAPICall(apiCall, attempt = 1) {
      try {
        return await apiCall();
      } catch (error) {
        if (attempt < this.retryAttempts) {
          console.log(`API call failed, retrying in ${this.retryDelay}ms (attempt ${attempt}/${this.retryAttempts})`);
          
          await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
          return this.retryAPICall(apiCall, attempt + 1);
        }
        throw error;
      }
    }

    validateOrder(order) {
      return order.order_id && 
             order.total_amount && 
             order.customer && 
             order.customer.email;
    }

    async syncOrderToExternalSystem(order) {
      // Transform Quickbutik order to your system's format
      const externalOrder = this.transformOrder(order);
      
      // Make API call to your external system
      const response = await fetch('https://your-system.com/api/orders', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.EXTERNAL_API_TOKEN}`
        },
        body: JSON.stringify(externalOrder)
      });

      if (!response.ok) {
        throw new Error(`External API error: ${response.status} ${response.statusText}`);
      }

      return response.json();
    }

    transformOrder(quickbutikOrder) {
      return {
        external_order_id: quickbutikOrder.order_id,
        customer_email: quickbutikOrder.customer.email,
        total_amount: parseFloat(quickbutikOrder.total_amount),
        currency: quickbutikOrder.payment?.currency || 'SEK',
        items: quickbutikOrder.products?.map(product => ({
          sku: product.sku,
          quantity: product.qty || 1,
          price: parseFloat(product.price || 0)
        })) || [],
        shipping_address: quickbutikOrder.customer.shipping_details,
        billing_address: quickbutikOrder.customer.billing_details,
        created_at: quickbutikOrder.date_created
      };
    }

    async handleOrderSyncFailure(orderId, error) {
      // In production, you might:
      // 1. Add to a retry queue (Redis, SQS, etc.)
      // 2. Send alerts to monitoring system
      // 3. Log to error tracking service
      
      console.error(`Order sync failed for ${orderId}:`, error.message);
      
      // Example: Add to retry queue
      // await this.addToRetryQueue({ orderId, error: error.message, timestamp: new Date() });
    }
  }

  // Usage
  const orderSync = new OrderSyncService(process.env.QUICKBUTIK_API_KEY);

  async function handleNewOrder(orderId) {
    await orderSync.handleNewOrder(orderId);
  }
  ```

  ```python Python theme={null}
  import asyncio
  import aiohttp
  import json
  from typing import Dict, List, Optional
  from quickbutik_api import QuickbutikAPI  # From previous tutorial

  class OrderSyncService:
      def __init__(self, api_key: str):
          self.api = QuickbutikAPI(api_key)
          self.retry_attempts = 3
          self.retry_delay = 1  # seconds
      
      async def handle_new_order(self, order_id: str):
          try:
              # Fetch complete order details
              orders = await self.retry_api_call(
                  lambda: self.api.get_orders(
                      order_id=order_id,
                      include_details=True,
                      apps_load=True
                  )
              )
              
              if not orders:
                  raise Exception(f'Order {order_id} not found')
              
              order = orders[0]
              
              # Validate order data
              if not self.validate_order(order):
                  raise Exception(f'Invalid order data for order {order_id}')
              
              # Sync to external system
              await self.sync_order_to_external_system(order)
              
              print(f'Successfully synced order {order_id} to external system')
              
          except Exception as error:
              print(f'Failed to process new order {order_id}: {error}')
              await self.handle_order_sync_failure(order_id, error)
      
      async def retry_api_call(self, api_call, attempt: int = 1):
          try:
              return await api_call()
          except Exception as error:
              if attempt < self.retry_attempts:
                  print(f'API call failed, retrying in {self.retry_delay * attempt}s (attempt {attempt}/{self.retry_attempts})')
                  await asyncio.sleep(self.retry_delay * attempt)
                  return await self.retry_api_call(api_call, attempt + 1)
              raise error
      
      def validate_order(self, order: Dict) -> bool:
          return (order.get('order_id') and 
                  order.get('total_amount') and 
                  order.get('customer') and 
                  order.get('customer', {}).get('email'))
      
      async def sync_order_to_external_system(self, order: Dict):
          # Transform Quickbutik order to your system's format
          external_order = self.transform_order(order)
          
          # Make API call to your external system
          async with aiohttp.ClientSession() as session:
              async with session.post(
                  'https://your-system.com/api/orders',
                  headers={
                      'Content-Type': 'application/json',
                      'Authorization': f'Bearer {os.getenv("EXTERNAL_API_TOKEN")}'
                  },
                  json=external_order
              ) as response:
                  if not response.ok:
                      raise Exception(f'External API error: {response.status} {await response.text()}')
                  return await response.json()
      
      def transform_order(self, quickbutik_order: Dict) -> Dict:
          return {
              'external_order_id': quickbutik_order['order_id'],
              'customer_email': quickbutik_order['customer']['email'],
              'total_amount': float(quickbutik_order['total_amount']),
              'currency': quickbutik_order.get('payment', {}).get('currency', 'SEK'),
              'items': [
                  {
                      'sku': product.get('sku'),
                      'quantity': product.get('qty', 1),
                      'price': float(product.get('price', 0))
                  }
                  for product in quickbutik_order.get('products', [])
              ],
              'shipping_address': quickbutik_order['customer'].get('shipping_details'),
              'billing_address': quickbutik_order['customer'].get('billing_details'),
              'created_at': quickbutik_order.get('date_created')
          }
      
      async def handle_order_sync_failure(self, order_id: str, error: Exception):
          # In production, you might:
          # 1. Add to a retry queue (Redis, Celery, etc.)
          # 2. Send alerts to monitoring system
          # 3. Log to error tracking service
          
          print(f'Order sync failed for {order_id}: {error}')
          
          # Example: Add to retry queue
          # await self.add_to_retry_queue({'order_id': order_id, 'error': str(error), 'timestamp': datetime.now()})

  # Usage
  order_sync = OrderSyncService(os.getenv('QUICKBUTIK_API_KEY'))

  async def handle_new_order(order_id: str):
      await order_sync.handle_new_order(order_id)
  ```
</CodeGroup>

## Part 3: Bidirectional Status Updates

Update Quickbutik when orders are processed in your external system:

<CodeGroup>
  ```javascript Node.js theme={null}
  class OrderStatusManager {
    constructor(apiKey) {
      this.api = new QuickbutikAPI(apiKey);
    }

    async markOrderAsShipped(orderId, shippingInfo) {
      try {
        const statusUpdate = {
          order_id: orderId,
          status: 'done',
          shipping_info: {
            trackingnumber: shippingInfo.trackingNumber,
            company: shippingInfo.carrier
          },
          email_confirmation: 'true'
        };

        const result = await this.api.updateOrderStatus(statusUpdate);
        
        console.log(`Order ${orderId} marked as shipped with tracking ${shippingInfo.trackingNumber}`);
        return result;

      } catch (error) {
        console.error(`Failed to update order ${orderId} status:`, error);
        throw error;
      }
    }

    async markOrderAsPaid(orderId, paymentInfo = {}) {
      try {
        const statusUpdate = {
          order_id: orderId,
          status: 'paid',
          skip_email_confirmation: paymentInfo.skipEmail ? 'true' : 'false'
        };

        const result = await this.api.updateOrderStatus(statusUpdate);
        
        console.log(`Order ${orderId} marked as paid`);
        return result;

      } catch (error) {
        console.error(`Failed to mark order ${orderId} as paid:`, error);
        throw error;
      }
    }

    async cancelOrder(orderId, reason = '') {
      try {
        const statusUpdate = {
          order_id: orderId,
          status: 'cancelled'
        };

        const result = await this.api.updateOrderStatus(statusUpdate);
        
        console.log(`Order ${orderId} cancelled. Reason: ${reason}`);
        return result;

      } catch (error) {
        console.error(`Failed to cancel order ${orderId}:`, error);
        throw error;
      }
    }
  }

  // Usage example - webhook from your external system
  app.post('/webhooks/external-system', async (req, res) => {
    const { event_type, order_id, data } = req.body;
    const statusManager = new OrderStatusManager(process.env.QUICKBUTIK_API_KEY);

    try {
      switch (event_type) {
        case 'order.shipped':
          await statusManager.markOrderAsShipped(order_id, {
            trackingNumber: data.tracking_number,
            carrier: data.carrier
          });
          break;
          
        case 'order.payment_confirmed':
          await statusManager.markOrderAsPaid(order_id);
          break;
          
        case 'order.cancelled':
          await statusManager.cancelOrder(order_id, data.reason);
          break;
      }

      res.status(200).json({ status: 'success' });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  });
  ```

  ```python Python theme={null}
  class OrderStatusManager:
      def __init__(self, api_key: str):
          self.api = QuickbutikAPI(api_key)
      
      async def mark_order_as_shipped(self, order_id: str, shipping_info: Dict):
          try:
              status_update = {
                  'order_id': order_id,
                  'status': 'done',
                  'shipping_info': {
                      'trackingnumber': shipping_info['tracking_number'],
                      'company': shipping_info['carrier']
                  },
                  'email_confirmation': 'true'
              }
              
              result = await self.api.update_order_status(status_update)
              
              print(f'Order {order_id} marked as shipped with tracking {shipping_info["tracking_number"]}')
              return result
              
          except Exception as error:
              print(f'Failed to update order {order_id} status: {error}')
              raise error
      
      async def mark_order_as_paid(self, order_id: str, payment_info: Dict = {}):
          try:
              status_update = {
                  'order_id': order_id,
                  'status': 'paid',
                  'skip_email_confirmation': 'true' if payment_info.get('skip_email') else 'false'
              }
              
              result = await self.api.update_order_status(status_update)
              
              print(f'Order {order_id} marked as paid')
              return result
              
          except Exception as error:
              print(f'Failed to mark order {order_id} as paid: {error}')
              raise error
      
      async def cancel_order(self, order_id: str, reason: str = ''):
          try:
              status_update = {
                  'order_id': order_id,
                  'status': 'cancelled'
              }
              
              result = await self.api.update_order_status(status_update)
              
              print(f'Order {order_id} cancelled. Reason: {reason}')
              return result
              
          except Exception as error:
              print(f'Failed to cancel order {order_id}: {error}')
              raise error

  # Usage example - webhook from your external system
  @app.route('/webhooks/external-system', methods=['POST'])
  async def external_system_webhook():
      data = request.get_json()
      event_type = data.get('event_type')
      order_id = data.get('order_id')
      event_data = data.get('data', {})
      
      status_manager = OrderStatusManager(os.getenv('QUICKBUTIK_API_KEY'))
      
      try:
          if event_type == 'order.shipped':
              await status_manager.mark_order_as_shipped(order_id, {
                  'tracking_number': event_data['tracking_number'],
                  'carrier': event_data['carrier']
              })
          elif event_type == 'order.payment_confirmed':
              await status_manager.mark_order_as_paid(order_id)
          elif event_type == 'order.cancelled':
              await status_manager.cancel_order(order_id, event_data.get('reason', ''))
          
          return jsonify({'status': 'success'}), 200
      except Exception as error:
          return jsonify({'error': str(error)}), 500
  ```
</CodeGroup>

## Part 4: Production Deployment

Here's a production-ready setup with Docker:

<CodeGroup>
  ```dockerfile Dockerfile theme={null}
  FROM node:18-alpine

  WORKDIR /app

  # Copy package files
  COPY package*.json ./
  RUN npm ci --only=production

  # Copy application code
  COPY . .

  # Create non-root user
  RUN addgroup -g 1001 -S nodejs
  RUN adduser -S nodejs -u 1001

  # Change ownership and switch to non-root user
  RUN chown -R nodejs:nodejs /app
  USER nodejs

  EXPOSE 3000

  # Health check
  HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

  CMD ["node", "server.js"]
  ```

  ```yaml docker-compose.yml theme={null}
  version: '3.8'

  services:
    order-sync:
      build: .
      ports:
        - "3000:3000"
      environment:
        - QUICKBUTIK_API_KEY=${QUICKBUTIK_API_KEY}
        - EXTERNAL_API_TOKEN=${EXTERNAL_API_TOKEN}
        - REDIS_URL=${REDIS_URL}
        - NODE_ENV=production
      depends_on:
        - redis
      restart: unless-stopped
      
    redis:
      image: redis:7-alpine
      ports:
        - "6379:6379"
      volumes:
        - redis_data:/data
      restart: unless-stopped

    nginx:
      image: nginx:alpine
      ports:
        - "80:80"
        - "443:443"
      volumes:
        - ./nginx.conf:/etc/nginx/nginx.conf
        - ./ssl:/etc/nginx/ssl
      depends_on:
        - order-sync
      restart: unless-stopped

  volumes:
    redis_data:
  ```
</CodeGroup>

## Part 5: Monitoring and Alerting

Add comprehensive monitoring to your integration:

<CodeGroup>
  ```javascript Monitoring theme={null}
  const prometheus = require('prom-client');

  // Create metrics
  const webhookCounter = new prometheus.Counter({
    name: 'quickbutik_webhooks_total',
    help: 'Total number of webhooks received',
    labelNames: ['event_type', 'status']
  });

  const orderSyncDuration = new prometheus.Histogram({
    name: 'order_sync_duration_seconds',
    help: 'Time spent syncing orders',
    buckets: [0.1, 0.5, 1, 2, 5, 10]
  });

  const orderSyncErrors = new prometheus.Counter({
    name: 'order_sync_errors_total',
    help: 'Total number of order sync errors',
    labelNames: ['error_type']
  });

  // Add metrics to webhook handler
  app.post('/webhooks/quickbutik', async (req, res) => {
    const { event_type, order_id } = req.query;
    const timer = orderSyncDuration.startTimer();
    
    try {
      webhookCounter.inc({ event_type, status: 'received' });
      
      res.status(200).send('OK');
      
      await processOrderWebhook(event_type, order_id);
      
      webhookCounter.inc({ event_type, status: 'processed' });
      timer();
      
    } catch (error) {
      orderSyncErrors.inc({ error_type: error.constructor.name });
      webhookCounter.inc({ event_type, status: 'error' });
      timer();
      
      // Send alert
      await sendAlert({
        type: 'order_sync_error',
        orderId,
        error: error.message,
        timestamp: new Date()
      });
    }
  });

  // Metrics endpoint
  app.get('/metrics', async (req, res) => {
    res.set('Content-Type', prometheus.register.contentType);
    res.end(await prometheus.register.metrics());
  });

  // Health check endpoint
  app.get('/health', (req, res) => {
    res.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime: process.uptime()
    });
  });

  async function sendAlert(alert) {
    // Send to Slack, PagerDuty, email, etc.
    console.error('ALERT:', alert);
    
    // Example: Slack webhook
    if (process.env.SLACK_WEBHOOK_URL) {
      await fetch(process.env.SLACK_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `🚨 Order sync error for order ${alert.orderId}: ${alert.error}`
        })
      });
    }
  }
  ```
</CodeGroup>

## 🎉 You Did It!

You now have a production-ready order synchronization system with:

* ✅ Webhook handling with proper acknowledgment
* ✅ Automatic retry logic for failed API calls
* ✅ Bidirectional order status updates
* ✅ Comprehensive error handling and monitoring
* ✅ Production deployment configuration

## Next Steps

<CardGroup cols={2}>
  <Card title="Inventory Management" icon="boxes-stacked" href="/api-v1/guides/inventory-management">
    Learn how to keep product stock levels synchronized
  </Card>
</CardGroup>
