Skip to Content
Welcome to Zendera Knowledge Hub
API DocumentationIntegration Patterns

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.

Last updated on