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.

Next Steps