Integration Patterns
Common integration patterns and workflows for the Zendera API suite, including complete examples and best practices.
Complete Order Lifecycle
1. Import Order with Hierarchical Products
Start by creating an order with a hierarchical product structure:
const orderData = {
externalId: 'ORDER_HIERARCHY_001',
date: '2024-01-16',
reference: 'Customer PO-12345',
customer: {
externalId: 'CUST_001',
businessName: 'Acme Corp',
},
vehicleType: { id: 5, name: 'Van' },
orderType: { id: 2, name: 'Delivery' },
pickup: {
name: 'Main Warehouse',
address: {
address1: 'Industrial Blvd 123',
city: 'Oslo',
postalCode: '0123',
country: 'Norway',
},
earliest: '2024-01-16T08:00:00Z',
latest: '2024-01-16T10:00:00Z',
},
delivery: {
name: 'Customer Site',
address: {
address1: 'Customer Street 456',
city: 'Bergen',
postalCode: '5000',
country: 'Norway',
},
earliest: '2024-01-16T14:00:00Z',
latest: '2024-01-16T16:00:00Z',
},
products: [
{
name: 'EUR Pallet',
externalId: 'PALLET_001',
atomType: 'colli',
quantity: 1,
weight: 25.0,
length: 120.0,
width: 80.0,
height: 180.0,
barcodeId: 'PALLET_BARCODE_001',
},
{
name: 'Product A - Electronics',
externalId: 'PROD_A_001',
parentExternalId: 'PALLET_001',
atomType: 'trade item',
quantity: 10,
weight: 2.0,
barcodeId: 'PROD_A_BARCODE',
},
{
name: 'Product B - Accessories',
externalId: 'PROD_B_001',
parentExternalId: 'PALLET_001',
atomType: 'trade item',
quantity: 5,
weight: 3.0,
barcodeId: 'PROD_B_BARCODE',
},
],
};
// Import the order
const importResponse = await fetch(`${API_BASE}/v3/orders/import`, {
method: 'POST',
headers: {
Authorization: `apikey ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
orders: [orderData],
updateConfig: {
orderLocation: 'REPLACE_EXISTING_ORDER_LOCATION_BEHAVIOR',
orderProducts: 'REPLACE_EXISTING_ORDER_PRODUCT',
allowMerge: false,
},
}),
});
const importResult = await importResponse.json();
const zenderaOrderId = importResult.results[0].orderCreated.OrderID;2. View Created Product Hierarchy
After import, visualize the created hierarchy:
// Get the atom tree structure
const atomResponse = await fetch(
`${API_BASE}/v1/atoms/${zenderaOrderId}/tree`,
{
headers: { Authorization: `apikey ${API_KEY}` },
}
);
const atomTree = await atomResponse.json();
// Visualize the hierarchy
function printHierarchy(atoms, level = 0) {
atoms.forEach(atom => {
const indent = ' '.repeat(level);
console.log(
`${indent}${atom.name} (${atom.type}) - Qty: ${atom.quantity} - ID: ${atom.id}`
);
if (atom.children && atom.children.length > 0) {
printHierarchy(atom.children, level + 1);
}
});
}
console.log('Order Hierarchy:');
printHierarchy(atomTree.atoms);3. Update Products and Hierarchy
Add or modify products after initial import:
// Add additional products to existing pallet
await fetch(`${API_BASE}/v1/order-products/upsert`, {
method: 'POST',
headers: {
Authorization: `apikey ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
create: [
{
externalOrderNumber: 'ORDER_HIERARCHY_001',
productRequest: {
name: 'Product C - Cables',
externalId: 'PROD_C_001',
parentExternalId: 'PALLET_001',
atomType: 'trade item',
quantity: 15,
weight: 1.5,
barcodeId: 'PROD_C_BARCODE',
},
},
],
updateConfig: {
orderProducts: 'REPLACE_EXISTING_ORDER_PRODUCT',
hierarchicalMode: true,
},
}),
});
// Update existing product quantity
await fetch(`${API_BASE}/v1/order-products/upsert`, {
method: 'POST',
headers: {
Authorization: `apikey ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
update: [
{
externalOrderNumber: 'ORDER_HIERARCHY_001',
productRequest: {
externalId: 'PROD_A_001',
quantity: 12, // Updated from 10 to 12
},
},
],
updateConfig: {
hierarchicalMode: true,
},
}),
});4. Monitor Order Status
Track the order through its lifecycle:
async function monitorOrderStatus(externalOrderNumber) {
const statusResponse = await fetch(
`${API_BASE}/v2/orders/summary/internal/${externalOrderNumber}`,
{ headers: { Authorization: `apikey ${API_KEY}` } }
);
const orderStatus = await statusResponse.json();
const order = orderStatus.order;
console.log(`Order Status: ${order.orderDetails.statusName}`);
console.log(
`Pickup Status: ${order.pickup?.details?.statusName || 'Not started'}`
);
console.log(
`Delivery Status: ${order.delivery?.details?.statusName || 'Not started'}`
);
if (order.driverDetails) {
console.log(`Assigned Driver: ${order.driverDetails.name}`);
}
return order;
}
// Monitor every 30 seconds
setInterval(() => {
monitorOrderStatus('ORDER_HIERARCHY_001');
}, 30000);Real-time Status Monitoring
Status Feed Integration
Implement real-time order tracking using the status feed:
class OrderStatusMonitor {
constructor(apiKey, baseUrl) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.token = '';
this.isRunning = false;
this.callbacks = new Map();
}
// Register callback for specific order
watchOrder(externalOrderNumber, callback) {
this.callbacks.set(externalOrderNumber, callback);
}
// Start monitoring status changes
async startMonitoring() {
this.isRunning = true;
while (this.isRunning) {
try {
const response = await fetch(
`${this.baseUrl}/v2/orders/summary/orderstatusfeed?token=${this.token}&pageSize=100`,
{
headers: { Authorization: `apikey ${this.apiKey}` },
}
);
const data = await response.json();
// Process status changes
if (data.orders) {
data.orders.forEach(order => {
const callback = this.callbacks.get(order.internalOrderNumber);
if (callback) {
callback(order);
}
});
}
// Update token for next request
if (data.nextToken) {
this.token = data.nextToken;
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, 30000));
} catch (error) {
console.error('Status monitoring error:', error);
await new Promise(resolve => setTimeout(resolve, 60000)); // Wait longer on error
}
}
}
stop() {
this.isRunning = false;
}
}
// Usage
const monitor = new OrderStatusMonitor(API_KEY, API_BASE);
// Watch specific orders
monitor.watchOrder('ORDER_HIERARCHY_001', order => {
console.log(
`Status update for ${order.internalOrderNumber}: ${order.statusName}`
);
// Trigger specific actions based on status
switch (order.statusName) {
case 'dispatched':
sendCustomerNotification(order, 'Your order is on the way');
break;
case 'complete':
updateERPSystem(order);
sendDeliveryConfirmation(order);
break;
}
});
monitor.startMonitoring();Hierarchical Product Management
Dynamic Hierarchy Reorganization
Move products between containers dynamically:
async function reorganizeProductHierarchy(orderId) {
// Get current hierarchy
const atomTree = await getAtomTree(orderId);
// Example: Move all electronics to one pallet, accessories to another
const pallets = atomTree.atoms.filter(atom => atom.type === 'colli');
const electronicsPallet = pallets.find(p => p.name.includes('Electronics'));
const accessoriesPallet = pallets.find(p => p.name.includes('Accessories'));
// Find all products
const allProducts = [];
function collectProducts(atoms) {
atoms.forEach(atom => {
if (atom.type === 'trade item') {
allProducts.push(atom);
}
if (atom.children) {
collectProducts(atom.children);
}
});
}
collectProducts(atomTree.atoms);
// Move products based on name pattern
for (const product of allProducts) {
let targetPalletId = null;
if (product.name.includes('Electronics')) {
targetPalletId = electronicsPallet?.id;
} else if (product.name.includes('Accessories')) {
targetPalletId = accessoriesPallet?.id;
}
if (targetPalletId && product.parentId !== targetPalletId) {
await fetch(`${API_BASE}/v1/atoms/${orderId}/tree`, {
method: 'PATCH',
headers: {
Authorization: `apikey ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: product.id,
parentId: targetPalletId,
}),
});
console.log(`Moved ${product.name} to target pallet`);
}
}
// Return updated hierarchy
return await getAtomTree(orderId);
}Complex Hierarchy Creation
Create nested container structures:
async function createComplexHierarchy(externalOrderNumber) {
// Create multiple pallets with nested boxes
const hierarchyOperations = {
create: [
// Main pallets
{
externalOrderNumber,
productRequest: {
name: 'Electronics Pallet',
externalId: 'PALLET_ELECTRONICS_001',
atomType: 'colli',
quantity: 1,
weight: 30.0,
length: 120.0,
width: 80.0,
height: 180.0,
},
},
{
externalOrderNumber,
productRequest: {
name: 'Accessories Pallet',
externalId: 'PALLET_ACCESSORIES_001',
atomType: 'colli',
quantity: 1,
weight: 25.0,
length: 120.0,
width: 80.0,
height: 150.0,
},
},
// Boxes within pallets
{
externalOrderNumber,
productRequest: {
name: 'Electronics Box A',
externalId: 'BOX_ELECTRONICS_A',
parentExternalId: 'PALLET_ELECTRONICS_001',
atomType: 'colli',
quantity: 1,
weight: 15.0,
length: 40.0,
width: 30.0,
height: 25.0,
},
},
{
externalOrderNumber,
productRequest: {
name: 'Electronics Box B',
externalId: 'BOX_ELECTRONICS_B',
parentExternalId: 'PALLET_ELECTRONICS_001',
atomType: 'colli',
quantity: 1,
weight: 12.0,
length: 40.0,
width: 30.0,
height: 20.0,
},
},
// Individual products
{
externalOrderNumber,
productRequest: {
name: 'Laptop Computer',
externalId: 'LAPTOP_001',
parentExternalId: 'BOX_ELECTRONICS_A',
atomType: 'trade item',
quantity: 5,
weight: 2.5,
},
},
{
externalOrderNumber,
productRequest: {
name: 'Tablet Device',
externalId: 'TABLET_001',
parentExternalId: 'BOX_ELECTRONICS_B',
atomType: 'trade item',
quantity: 8,
weight: 0.8,
},
},
{
externalOrderNumber,
productRequest: {
name: 'Power Cables',
externalId: 'CABLES_001',
parentExternalId: 'PALLET_ACCESSORIES_001',
atomType: 'trade item',
quantity: 25,
weight: 0.5,
},
},
],
updateConfig: {
hierarchicalMode: true,
orderProducts: 'REPLACE_EXISTING_ORDER_PRODUCT',
},
};
const response = await fetch(`${API_BASE}/v1/order-products/upsert`, {
method: 'POST',
headers: {
Authorization: `apikey ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(hierarchyOperations),
});
return await response.json();
}ERP Integration Patterns
Bidirectional Sync
Synchronize data between your ERP system and Zendera:
class ERPZenderaSync {
constructor(erpApi, zenderaApi) {
this.erp = erpApi;
this.zendera = zenderaApi;
}
// Sync order from ERP to Zendera
async syncOrderToZendera(erpOrderId) {
// Get order from ERP
const erpOrder = await this.erp.getOrder(erpOrderId);
// Transform to Zendera format
const zenderaOrder = this.transformERPOrder(erpOrder);
// Import to Zendera
const importResult = await this.zendera.importOrders([zenderaOrder]);
// Update ERP with Zendera order ID
await this.erp.updateOrder(erpOrderId, {
zenderaOrderId: importResult.results[0].orderCreated.OrderID,
zenderaExternalId: zenderaOrder.externalId,
});
return importResult;
}
// Sync status back to ERP
async syncStatusToERP(zenderaExternalOrderId) {
// Get current status from Zendera
const orderStatus = await this.zendera.getOrderSummary(
zenderaExternalOrderId
);
// Map Zendera status to ERP status
const erpStatus = this.mapZenderaStatusToERP(
orderStatus.order.orderDetails.statusName
);
// Update ERP
await this.erp.updateOrderStatus(zenderaExternalOrderId, erpStatus);
return erpStatus;
}
transformERPOrder(erpOrder) {
return {
externalId: erpOrder.orderNumber,
date: erpOrder.deliveryDate,
reference: erpOrder.customerReference,
customer: {
externalId: erpOrder.customerId,
businessName: erpOrder.customerName,
},
vehicleType: { id: this.mapERPVehicleType(erpOrder.vehicleRequirement) },
orderType: { id: 2 }, // Delivery
pickup: {
name: erpOrder.warehouse.name,
address: erpOrder.warehouse.address,
},
delivery: {
name: erpOrder.customer.name,
address: erpOrder.customer.address,
},
products: erpOrder.items.map(item => ({
name: item.productName,
externalId: item.sku,
quantity: item.quantity,
weight: item.weight,
atomType: item.isPackaged ? 'colli' : 'trade item',
})),
};
}
mapZenderaStatusToERP(zenderaStatus) {
const statusMap = {
unassigned: 'PENDING',
planned: 'SCHEDULED',
dispatched: 'IN_TRANSIT',
in_transit: 'IN_TRANSIT',
arrived: 'ARRIVED',
delivered: 'DELIVERED',
complete: 'COMPLETED',
cancelled: 'CANCELLED',
};
return statusMap[zenderaStatus] || 'UNKNOWN';
}
}Batch Processing
Handle large volumes of orders efficiently:
class BatchOrderProcessor {
constructor(zenderaApi, batchSize = 50) {
this.api = zenderaApi;
this.batchSize = batchSize;
}
async processBatchFromERP(erpOrders) {
const results = [];
// Process in batches to avoid API limits
for (let i = 0; i < erpOrders.length; i += this.batchSize) {
const batch = erpOrders.slice(i, i + this.batchSize);
console.log(
`Processing batch ${Math.floor(i / this.batchSize) + 1} of ${Math.ceil(erpOrders.length / this.batchSize)}`
);
// Transform ERP orders to Zendera format
const zenderaOrders = batch.map(erpOrder =>
this.transformOrder(erpOrder)
);
try {
// Import batch
const importResult = await this.api.importOrders(zenderaOrders);
results.push(...importResult.results);
// Small delay between batches
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(
`Batch ${Math.floor(i / this.batchSize) + 1} failed:`,
error
);
// Try individual orders in failed batch
for (const order of zenderaOrders) {
try {
const singleResult = await this.api.importOrders([order]);
results.push(...singleResult.results);
} catch (singleError) {
console.error(`Order ${order.externalId} failed:`, singleError);
results.push({
error: true,
externalId: order.externalId,
message: singleError.message,
});
}
}
}
}
return results;
}
// Generate processing report
generateReport(results) {
const report = {
total: results.length,
successful: 0,
failed: 0,
errors: [],
};
results.forEach(result => {
if (result.error) {
report.failed++;
report.errors.push(result);
} else {
report.successful++;
}
});
return report;
}
}Error Handling & Resilience
Retry Logic with Exponential Backoff
class ResilientZenderaAPI {
constructor(apiKey, baseUrl, maxRetries = 3) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.maxRetries = maxRetries;
}
async makeRequest(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const requestOptions = {
...options,
headers: {
Authorization: `apikey ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers,
},
};
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const response = await fetch(url, requestOptions);
if (response.ok) {
return await response.json();
}
// Handle specific HTTP errors
if (response.status === 429) {
// Rate limited
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.log(
`Rate limited, waiting ${delay}ms before retry ${attempt}`
);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (response.status >= 500) {
// Server error
if (attempt < this.maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
console.log(
`Server error ${response.status}, retrying in ${delay}ms`
);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
// Client error (4xx) - don't retry
const errorData = await response.json().catch(() => ({}));
throw new Error(
`API Error ${response.status}: ${errorData.message || response.statusText}`
);
} catch (error) {
if (attempt === this.maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed:`, error.message);
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
async importOrders(orders) {
return this.makeRequest('/v3/orders/import', {
method: 'POST',
body: JSON.stringify({ orders }),
});
}
async getOrderSummary(externalOrderNumber) {
return this.makeRequest(
`/v2/orders/summary/internal/${externalOrderNumber}`
);
}
}Performance Optimization
Connection Pooling and Caching
class OptimizedZenderaClient {
constructor(apiKey, baseUrl) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.cache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
// Configure HTTP agent for connection pooling
this.agent = new require('https').Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000,
freeSocketTimeout: 30000,
});
}
// Cached order summary requests
async getOrderSummary(externalOrderNumber, useCache = true) {
const cacheKey = `order_summary_${externalOrderNumber}`;
if (useCache) {
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
}
const data = await this.makeRequest(
`/v2/orders/summary/internal/${externalOrderNumber}`
);
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
});
return data;
}
// Bulk operations with concurrency control
async bulkGetOrderSummaries(externalOrderNumbers, concurrency = 10) {
const semaphore = new Semaphore(concurrency);
const promises = externalOrderNumbers.map(async orderNumber => {
await semaphore.acquire();
try {
return await this.getOrderSummary(orderNumber);
} finally {
semaphore.release();
}
});
return await Promise.all(promises);
}
}
// Simple semaphore implementation
class Semaphore {
constructor(max) {
this.max = max;
this.current = 0;
this.queue = [];
}
async acquire() {
if (this.current < this.max) {
this.current++;
return;
}
return new Promise(resolve => {
this.queue.push(resolve);
});
}
release() {
this.current--;
if (this.queue.length > 0) {
const resolve = this.queue.shift();
this.current++;
resolve();
}
}
}These integration patterns provide comprehensive examples for building robust, scalable integrations with the Zendera API suite. Each pattern addresses common real-world scenarios and includes error handling, performance optimization, and best practices.