Guides
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.
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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
Assistant
Responses are generated using AI and may contain mistakes.