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');
});
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)
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);
}
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)
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 });
}
});
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
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"]
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:
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
Inventory Management
Learn how to keep product stock levels synchronized