Inventory Management
Keep your product stock levels synchronized across platforms
Learn how to build a robust inventory synchronization system that keeps stock levels accurate across Quickbutik and your external systems in real-time.
Use cases covered:
- Real-time stock level synchronization
- Bulk inventory updates
- Low stock alerts and management
- Multi-location inventory tracking
- Prevention of overselling
Quick Example
Here’s a complete example of syncing inventory when stock changes:
const QuickbutikAPI = require('./quickbutik-api');
class InventoryManager {
constructor(apiKey) {
this.api = new QuickbutikAPI(apiKey);
}
async updateProductStock(sku, newQuantity, location = 'main') {
try {
const result = await this.api.updateProducts({
sku: sku,
stock: newQuantity,
qty_location: location
});
console.log(`Updated ${sku} stock to ${newQuantity} at ${location}`);
return result;
} catch (error) {
console.error(`Failed to update stock for ${sku}:`, error);
throw error;
}
}
async bulkUpdateInventory(inventoryUpdates) {
const results = [];
const batchSize = 10;
for (let i = 0; i < inventoryUpdates.length; i += batchSize) {
const batch = inventoryUpdates.slice(i, i + batchSize);
const batchPromises = batch.map(update =>
this.updateProductStock(update.sku, update.quantity, update.location)
.catch(error => ({ error: error.message, sku: update.sku }))
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Small delay to avoid rate limiting
if (i + batchSize < inventoryUpdates.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return results;
}
}
// Usage
const inventory = new InventoryManager(process.env.QUICKBUTIK_API_KEY);
// Update single product
await inventory.updateProductStock('SHIRT-123', 45);
// Bulk update
await inventory.bulkUpdateInventory([
{ sku: 'SHIRT-123', quantity: 45, location: 'warehouse-1' },
{ sku: 'PANTS-456', quantity: 12, location: 'warehouse-1' },
{ sku: 'HAT-789', quantity: 0, location: 'warehouse-2' }
]);
Webhook-Based Inventory Sync
Set up real-time inventory synchronization using webhooks:
app.post('/webhooks/quickbutik', async (req, res) => {
const { event_type, product_id } = req.query;
// Acknowledge webhook immediately
res.status(200).send('OK');
if (event_type === 'product.update') {
await handleProductUpdate(product_id);
}
});
async function handleProductUpdate(productId) {
try {
// Fetch updated product details
const products = await api.getProducts({
product_id: productId,
include_details: true
});
if (!products || products.length === 0) {
throw new Error(`Product ${productId} not found`);
}
const product = products[0];
// Check if stock changed
if (await hasStockChanged(product)) {
await syncInventoryToExternalSystem(product);
}
} catch (error) {
console.error(`Failed to handle product update for ${productId}:`, error);
}
}
async function hasStockChanged(product) {
// Compare with your local inventory database
const localInventory = await getLocalInventory(product.sku);
return localInventory.quantity !== parseInt(product.qty || 0);
}
async function syncInventoryToExternalSystem(product) {
try {
const inventoryUpdate = {
sku: product.sku,
quantity: parseInt(product.qty || 0),
location: product.qty_location || 'default',
updated_at: new Date().toISOString()
};
const response = await fetch('https://your-system.com/api/inventory', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EXTERNAL_API_TOKEN}`
},
body: JSON.stringify(inventoryUpdate)
});
if (!response.ok) {
throw new Error(`External API error: ${response.status}`);
}
console.log(`Synced inventory for ${product.sku}: ${product.qty} units`);
} catch (error) {
console.error(`Failed to sync inventory for ${product.sku}:`, error);
// Add to retry queue
await addToRetryQueue({
type: 'inventory_sync',
product,
timestamp: new Date()
});
}
}
Low Stock Monitoring
Implement automated low stock alerts and reordering:
class LowStockMonitor {
constructor(apiKey, thresholds = {}) {
this.api = new QuickbutikAPI(apiKey);
this.defaultThreshold = thresholds.default || 10;
this.customThresholds = thresholds.custom || {};
}
async checkLowStock() {
try {
const products = await this.api.getProducts({
include_details: true,
limit: 500
});
const lowStockProducts = [];
for (const product of products) {
if (product.variants && product.variants.length > 0) {
// Check variants
for (const variant of product.variants) {
if (this.isLowStock(variant)) {
lowStockProducts.push({
...variant,
parent_sku: product.sku,
parent_title: product.title
});
}
}
} else {
// Check main product
if (this.isLowStock(product)) {
lowStockProducts.push(product);
}
}
}
if (lowStockProducts.length > 0) {
await this.handleLowStockAlert(lowStockProducts);
}
return lowStockProducts;
} catch (error) {
console.error('Failed to check low stock:', error);
throw error;
}
}
isLowStock(product) {
const currentStock = parseInt(product.qty || 0);
const threshold = this.customThresholds[product.sku] || this.defaultThreshold;
return currentStock > 0 && currentStock <= threshold;
}
async handleLowStockAlert(lowStockProducts) {
// Send alert notification
await this.sendLowStockNotification(lowStockProducts);
// Auto-reorder if configured
const autoReorderProducts = lowStockProducts.filter(p =>
this.shouldAutoReorder(p)
);
if (autoReorderProducts.length > 0) {
await this.createReorderRequests(autoReorderProducts);
}
}
async sendLowStockNotification(products) {
const message = `🚨 Low Stock Alert!\n\n${products.map(p =>
`${p.title || p.parent_title} (${p.sku}): ${p.qty} units remaining`
).join('\n')}`;
// Send to Slack
if (process.env.SLACK_WEBHOOK_URL) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
// Send email
if (process.env.EMAIL_WEBHOOK_URL) {
await fetch(process.env.EMAIL_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: process.env.INVENTORY_EMAIL,
subject: 'Low Stock Alert',
body: message
})
});
}
}
shouldAutoReorder(product) {
// Add your auto-reorder logic here
return product.supplier_sku && product.auto_reorder_enabled;
}
async createReorderRequests(products) {
for (const product of products) {
try {
const reorderQuantity = this.calculateReorderQuantity(product);
await this.createPurchaseOrder({
sku: product.sku,
supplier_sku: product.supplier_sku,
quantity: reorderQuantity,
supplier: product.supplier_name
});
console.log(`Created reorder for ${product.sku}: ${reorderQuantity} units`);
} catch (error) {
console.error(`Failed to create reorder for ${product.sku}:`, error);
}
}
}
calculateReorderQuantity(product) {
// Simple reorder logic - you can make this more sophisticated
const currentStock = parseInt(product.qty || 0);
const threshold = this.customThresholds[product.sku] || this.defaultThreshold;
const targetStock = threshold * 3; // Reorder to 3x threshold
return Math.max(targetStock - currentStock, threshold);
}
}
// Usage
const monitor = new LowStockMonitor(process.env.QUICKBUTIK_API_KEY, {
default: 10,
custom: {
'POPULAR-ITEM-123': 50,
'SEASONAL-456': 5
}
});
// Run every hour
setInterval(async () => {
try {
const lowStockProducts = await monitor.checkLowStock();
console.log(`Found ${lowStockProducts.length} low stock products`);
} catch (error) {
console.error('Low stock check failed:', error);
}
}, 60 * 60 * 1000);
Multi-Location Inventory
Handle inventory across multiple locations:
class MultiLocationInventory {
constructor(apiKey) {
this.api = new QuickbutikAPI(apiKey);
this.locations = {
'warehouse-1': { name: 'Main Warehouse', priority: 1 },
'warehouse-2': { name: 'Secondary Warehouse', priority: 2 },
'store-front': { name: 'Physical Store', priority: 3 }
};
}
async getInventoryByLocation(sku) {
try {
const products = await this.api.getProducts({
product_id: sku,
include_details: true
});
if (!products || products.length === 0) {
return null;
}
const product = products[0];
const inventory = {};
// If product has variants, aggregate by location
if (product.variants && product.variants.length > 0) {
for (const variant of product.variants) {
const location = variant.qty_location || 'default';
const quantity = parseInt(variant.qty || 0);
inventory[location] = (inventory[location] || 0) + quantity;
}
} else {
const location = product.qty_location || 'default';
inventory[location] = parseInt(product.qty || 0);
}
return {
sku: product.sku,
title: product.title,
total_quantity: Object.values(inventory).reduce((sum, qty) => sum + qty, 0),
locations: inventory
};
} catch (error) {
console.error(`Failed to get inventory for ${sku}:`, error);
throw error;
}
}
async transferStock(sku, fromLocation, toLocation, quantity) {
try {
// Get current inventory
const inventory = await this.getInventoryByLocation(sku);
if (!inventory || !inventory.locations[fromLocation]) {
throw new Error(`No inventory found for ${sku} at ${fromLocation}`);
}
if (inventory.locations[fromLocation] < quantity) {
throw new Error(`Insufficient stock at ${fromLocation}. Available: ${inventory.locations[fromLocation]}, Requested: ${quantity}`);
}
// Update source location (decrease)
await this.updateLocationStock(sku, fromLocation, inventory.locations[fromLocation] - quantity);
// Update destination location (increase)
const currentDestStock = inventory.locations[toLocation] || 0;
await this.updateLocationStock(sku, toLocation, currentDestStock + quantity);
console.log(`Transferred ${quantity} units of ${sku} from ${fromLocation} to ${toLocation}`);
return {
sku,
quantity,
from: fromLocation,
to: toLocation,
timestamp: new Date().toISOString()
};
} catch (error) {
console.error(`Failed to transfer stock for ${sku}:`, error);
throw error;
}
}
async updateLocationStock(sku, location, newQuantity) {
return this.api.updateProducts({
sku,
stock: newQuantity,
qty_location: location
});
}
async getOptimalFulfillmentLocation(sku, requestedQuantity) {
const inventory = await this.getInventoryByLocation(sku);
if (!inventory) {
return null;
}
// Sort locations by priority and availability
const availableLocations = Object.entries(inventory.locations)
.filter(([location, quantity]) => quantity >= requestedQuantity)
.map(([location, quantity]) => ({
location,
quantity,
priority: this.locations[location]?.priority || 999
}))
.sort((a, b) => a.priority - b.priority);
return availableLocations.length > 0 ? availableLocations[0] : null;
}
}
// Usage
const multiLocation = new MultiLocationInventory(process.env.QUICKBUTIK_API_KEY);
// Get inventory across all locations
const inventory = await multiLocation.getInventoryByLocation('SHIRT-123');
console.log('Inventory:', inventory);
// Transfer stock between locations
await multiLocation.transferStock('SHIRT-123', 'warehouse-1', 'store-front', 5);
// Find optimal fulfillment location
const optimal = await multiLocation.getOptimalFulfillmentLocation('SHIRT-123', 10);
console.log('Optimal location:', optimal);
Preventing Overselling
Implement safeguards to prevent overselling:
class OversellProtection {
constructor(apiKey) {
this.api = new QuickbutikAPI(apiKey);
this.safetyBuffer = 1; // Keep 1 unit as safety buffer
this.checkInterval = 5 * 60 * 1000; // Check every 5 minutes
}
async enableOversellProtection() {
// Start monitoring
setInterval(() => {
this.checkAndProtectInventory();
}, this.checkInterval);
console.log('Oversell protection enabled');
}
async checkAndProtectInventory() {
try {
const products = await this.api.getProducts({
include_details: true,
limit: 500
});
const protectionActions = [];
for (const product of products) {
// Skip if minus quantity is already disabled
if (product.disable_minusqty === '1') {
continue;
}
const currentStock = parseInt(product.qty || 0);
if (currentStock <= this.safetyBuffer) {
protectionActions.push({
sku: product.sku,
title: product.title,
currentStock,
action: 'disable_minus_qty'
});
await this.protectProduct(product.sku);
}
}
if (protectionActions.length > 0) {
await this.notifyProtectionActions(protectionActions);
}
} catch (error) {
console.error('Failed to check oversell protection:', error);
}
}
async protectProduct(sku) {
try {
await this.api.updateProducts({
sku,
disable_minusqty: '1'
});
console.log(`Enabled oversell protection for ${sku}`);
} catch (error) {
console.error(`Failed to protect product ${sku}:`, error);
}
}
async unprotectProduct(sku) {
try {
await this.api.updateProducts({
sku,
disable_minusqty: '0'
});
console.log(`Disabled oversell protection for ${sku}`);
} catch (error) {
console.error(`Failed to unprotect product ${sku}:`, error);
}
}
async notifyProtectionActions(actions) {
const message = `🛡️ Oversell Protection Activated!\n\n${actions.map(action =>
`${action.title} (${action.sku}): ${action.currentStock} units remaining - Protection enabled`
).join('\n')}`;
// Send notification
if (process.env.SLACK_WEBHOOK_URL) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
}
async manualStockCheck(sku) {
try {
const products = await this.api.getProducts({
product_id: sku,
include_details: true
});
if (!products || products.length === 0) {
throw new Error(`Product ${sku} not found`);
}
const product = products[0];
const currentStock = parseInt(product.qty || 0);
const isProtected = product.disable_minusqty === '1';
return {
sku: product.sku,
title: product.title,
currentStock,
isProtected,
recommendedAction: this.getRecommendedAction(currentStock, isProtected)
};
} catch (error) {
console.error(`Failed manual stock check for ${sku}:`, error);
throw error;
}
}
getRecommendedAction(currentStock, isProtected) {
if (currentStock <= this.safetyBuffer && !isProtected) {
return 'ENABLE_PROTECTION';
} else if (currentStock > this.safetyBuffer && isProtected) {
return 'DISABLE_PROTECTION';
} else {
return 'NO_ACTION_NEEDED';
}
}
}
// Usage
const protection = new OversellProtection(process.env.QUICKBUTIK_API_KEY);
// Enable automatic protection
await protection.enableOversellProtection();
// Manual check for specific product
const status = await protection.manualStockCheck('SHIRT-123');
console.log('Stock status:', status);
if (status.recommendedAction === 'ENABLE_PROTECTION') {
await protection.protectProduct(status.sku);
} else if (status.recommendedAction === 'DISABLE_PROTECTION') {
await protection.unprotectProduct(status.sku);
}
🎯 Best Practices
Batch Processing
Process inventory updates in batches to avoid rate limiting and improve performance.
Error Recovery
Implement retry logic for failed inventory updates with exponential backoff.
Data Validation
Validate inventory data before updates to prevent incorrect stock levels.
Monitoring
Monitor inventory sync health with metrics and alerts for critical failures.