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.

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

Architecture Overview

Part 1: Setting Up Webhook Endpoint

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

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');
});

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

Part 2: Fetching Order Details

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

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);
}

Part 3: Bidirectional Status Updates

Update Quickbutik when orders are processed in your external system:

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 });
  }
});

Part 4: Production Deployment

Here’s a production-ready setup with Docker:

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"]

Part 5: Monitoring and Alerting

Add comprehensive monitoring to your integration:

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}`
      })
    });
  }
}

🎉 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