> ## Documentation Index
> Fetch the complete documentation index at: https://quickbutik.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

<Info>
  **Use cases covered:**

  * Real-time stock level synchronization
  * Bulk inventory updates
  * Low stock alerts and management
  * Multi-location inventory tracking
  * Prevention of overselling
</Info>

## Quick Example

Here's a complete example of syncing inventory when stock changes:

<CodeGroup>
  ```javascript Node.js theme={null}
  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' }
  ]);
  ```

  ```python Python theme={null}
  import asyncio
  import time
  from typing import List, Dict
  from quickbutik_api import QuickbutikAPI

  class InventoryManager:
      def __init__(self, api_key: str):
          self.api = QuickbutikAPI(api_key)
      
      async def update_product_stock(self, sku: str, new_quantity: int, location: str = 'main'):
          try:
              result = await self.api.update_products({
                  'sku': sku,
                  'stock': new_quantity,
                  'qty_location': location
              })
              
              print(f'Updated {sku} stock to {new_quantity} at {location}')
              return result
          except Exception as error:
              print(f'Failed to update stock for {sku}: {error}')
              raise error
      
      async def bulk_update_inventory(self, inventory_updates: List[Dict]):
          results = []
          batch_size = 10
          
          for i in range(0, len(inventory_updates), batch_size):
              batch = inventory_updates[i:i + batch_size]
              
              batch_tasks = []
              for update in batch:
                  task = self.update_product_stock(
                      update['sku'], 
                      update['quantity'], 
                      update.get('location', 'main')
                  )
                  batch_tasks.append(task)
              
              try:
                  batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
                  results.extend(batch_results)
              except Exception as error:
                  print(f'Batch update error: {error}')
              
              # Small delay to avoid rate limiting
              if i + batch_size < len(inventory_updates):
                  await asyncio.sleep(0.1)
          
          return results

  # Usage
  async def main():
      inventory = InventoryManager(os.getenv('QUICKBUTIK_API_KEY'))
      
      # Update single product
      await inventory.update_product_stock('SHIRT-123', 45)
      
      # Bulk update
      await inventory.bulk_update_inventory([
          {'sku': 'SHIRT-123', 'quantity': 45, 'location': 'warehouse-1'},
          {'sku': 'PANTS-456', 'quantity': 12, 'location': 'warehouse-1'},
          {'sku': 'HAT-789', 'quantity': 0, 'location': 'warehouse-2'}
      ])

  asyncio.run(main())
  ```
</CodeGroup>

## Webhook-Based Inventory Sync

Set up real-time inventory synchronization using webhooks:

<CodeGroup>
  ```javascript Express.js Webhook Handler theme={null}
  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()
      });
    }
  }
  ```

  ```python Flask Webhook Handler theme={null}
  @app.route('/webhooks/quickbutik', methods=['POST'])
  def quickbutik_webhook():
      event_type = request.args.get('event_type')
      product_id = request.args.get('product_id')
      
      # Acknowledge webhook immediately
      response = jsonify({'status': 'received'})
      
      if event_type == 'product.update':
          asyncio.create_task(handle_product_update(product_id))
      
      return response, 200

  async def handle_product_update(product_id):
      try:
          # Fetch updated product details
          products = await api.get_products(
              product_id=product_id,
              include_details=True
          )
          
          if not products:
              raise Exception(f'Product {product_id} not found')
          
          product = products[0]
          
          # Check if stock changed
          if await has_stock_changed(product):
              await sync_inventory_to_external_system(product)
      
      except Exception as error:
          app.logger.error(f'Failed to handle product update for {product_id}: {error}')

  async def has_stock_changed(product):
      # Compare with your local inventory database
      local_inventory = await get_local_inventory(product['sku'])
      return local_inventory['quantity'] != int(product.get('qty', 0))

  async def sync_inventory_to_external_system(product):
      try:
          inventory_update = {
              'sku': product['sku'],
              'quantity': int(product.get('qty', 0)),
              'location': product.get('qty_location', 'default'),
              'updated_at': datetime.now().isoformat()
          }
          
          async with aiohttp.ClientSession() as session:
              async with session.put(
                  'https://your-system.com/api/inventory',
                  headers={
                      'Content-Type': 'application/json',
                      'Authorization': f'Bearer {os.getenv("EXTERNAL_API_TOKEN")}'
                  },
                  json=inventory_update
              ) as response:
                  if not response.ok:
                      raise Exception(f'External API error: {response.status}')
          
          app.logger.info(f'Synced inventory for {product["sku"]}: {product.get("qty", 0)} units')
          
      except Exception as error:
          app.logger.error(f'Failed to sync inventory for {product["sku"]}: {error}')
          
          # Add to retry queue
          await add_to_retry_queue({
              'type': 'inventory_sync',
              'product': product,
              'timestamp': datetime.now()
          })
  ```
</CodeGroup>

## Low Stock Monitoring

Implement automated low stock alerts and reordering:

<CodeGroup>
  ```javascript Low Stock Monitor theme={null}
  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);
  ```
</CodeGroup>

## Multi-Location Inventory

Handle inventory across multiple locations:

<CodeGroup>
  ```javascript Multi-Location Manager theme={null}
  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);
  ```
</CodeGroup>

## Preventing Overselling

Implement safeguards to prevent overselling:

<CodeGroup>
  ```javascript Overselling Prevention theme={null}
  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);
  }
  ```
</CodeGroup>

## 🎯 Best Practices

<CardGroup cols={2}>
  <Card title="Batch Processing" icon="layer-group">
    **Process inventory updates in batches** to avoid rate limiting and improve performance.
  </Card>

  <Card title="Error Recovery" icon="arrows-rotate">
    **Implement retry logic** for failed inventory updates with exponential backoff.
  </Card>

  <Card title="Data Validation" icon="shield-check">
    **Validate inventory data** before updates to prevent incorrect stock levels.
  </Card>

  <Card title="Monitoring" icon="chart-line">
    **Monitor inventory sync health** with metrics and alerts for critical failures.
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Product Catalog Sync" icon="box" href="/api-v1/guides/product-catalog-sync">
    Learn how to sync complete product catalogs between systems
  </Card>
</CardGroup>
