Skip to main content

ATP Monitoring and Low-Stock Alerts

This guide shows you how to pull Available to Promise (ATP) and availability data, then build a low-stock monitoring system.

Prerequisites

  • API Key: Valid API key with inventory.read permission
  • Organization Access: Access to your organization’s inventory data
  • Location ID: Location ID to monitor (optional, omit for all locations)
  • Channel IDs: Channel IDs to monitor (optional)

Step-by-Step Guide

Step 1: Get Channel × Location ATP Matrix

Retrieve ATP values for all products across channels at a specific location. Request:
curl -X GET "https://app.betterdata.co/api/inventory/channel-location?locationId=loc_123&page=1&limit=100" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"
Response:
{
  "products": [
    {
      "productMasterId": "prod_123",
      "productName": "Product Name",
      "globalSku": "SKU-123",
      "channels": [
        {
          "channelId": "DTC",
          "channelName": "Direct to Consumer",
          "atp": 150,
          "onHand": 200,
          "reserved": 50,
          "available": 150,
          "severity": "OK",
          "threshold": 10,
          "policyId": "policy_123"
        },
        {
          "channelId": "WHOLESALE",
          "channelName": "Wholesale",
          "atp": 5,
          "onHand": 20,
          "reserved": 15,
          "available": 5,
          "severity": "LOW",
          "threshold": 10,
          "policyId": "policy_456"
        }
      ],
      "totalOnHand": 220,
      "totalReserved": 65,
      "minAtp": 5,
      "maxAtp": 150,
      "hasCritical": false
    }
  ],
  "channels": [
    {
      "channelId": "DTC",
      "channelName": "Direct to Consumer",
      "channelType": "E_COMMERCE",
      "policyId": "policy_123",
      "lowAtpThreshold": 10
    }
  ],
  "location": {
    "id": "loc_123",
    "name": "Main Warehouse"
  },
  "pagination": {
    "page": 1,
    "pageSize": 100,
    "total": 500,
    "totalPages": 5
  }
}
Notes:
  • Severity Levels: OK, LOW, CRITICAL
  • ATP Calculation: ATP = On-Hand - Reserved - Safety Stock
  • Thresholds: Defined in channel policies
  • Pagination: Use page and limit to retrieve all products

Step 2: Identify Low-Stock Items

Filter products with low or critical ATP. Example Logic:
function identifyLowStock(products) {
  const lowStock = [];
  
  for (const product of products) {
    // Check each channel
    for (const channel of product.channels) {
      if (channel.severity === 'LOW' || channel.severity === 'CRITICAL') {
        lowStock.push({
          productMasterId: product.productMasterId,
          productName: product.productName,
          sku: product.globalSku,
          channelId: channel.channelId,
          channelName: channel.channelName,
          atp: channel.atp,
          threshold: channel.threshold,
          severity: channel.severity,
          onHand: channel.onHand,
          reserved: channel.reserved
        });
      }
    }
    
    // Also check overall minimum ATP
    if (product.minAtp <= 0) {
      lowStock.push({
        productMasterId: product.productMasterId,
        productName: product.productName,
        sku: product.globalSku,
        channelId: 'ALL',
        channelName: 'All Channels',
        atp: product.minAtp,
        severity: 'CRITICAL',
        onHand: product.totalOnHand,
        reserved: product.totalReserved
      });
    }
  }
  
  return lowStock;
}

Step 3: Get Detailed Inventory Levels

For low-stock items, get detailed inventory information. Request:
curl -X GET "https://app.betterdata.co/api/inventory?locationId=loc_123&productMasterId=prod_123" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"
Response:
{
  "items": [
    {
      "id": "item_123",
      "productMasterId": "prod_123",
      "locationId": "loc_123",
      "quantityOnHand": 200,
      "quantityReserved": 50,
      "quantityAvailable": 150,
      "lotId": "lot_123",
      "expiryDate": "2024-12-31",
      "binId": "bin_123"
    }
  ],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "total": 1,
    "totalPages": 1
  }
}

Step 4: Set Up Monitoring Loop

Create a monitoring script that periodically checks ATP levels. Example Script:
#!/bin/bash

API_KEY="YOUR_API_KEY"
BASE_URL="https://app.betterdata.co/api"
LOCATION_ID="loc_123"
ALERT_THRESHOLD=10  # Alert if ATP < 10

# Function to check ATP and alert on low stock
check_atp() {
  PAGE=1
  LOW_STOCK_COUNT=0
  
  while true; do
    RESPONSE=$(curl -s -X GET "${BASE_URL}/inventory/channel-location?locationId=${LOCATION_ID}&page=${PAGE}&limit=100" \
      -H "Authorization: Bearer ${API_KEY}" \
      -H "Content-Type: application/json")
    
    PRODUCTS=$(echo $RESPONSE | jq -r '.products[]')
    TOTAL_PAGES=$(echo $RESPONSE | jq -r '.pagination.totalPages')
    
    # Process each product
    echo $PRODUCTS | jq -c '.' | while read product; do
      PRODUCT_NAME=$(echo $product | jq -r '.productName')
      SKU=$(echo $product | jq -r '.globalSku')
      MIN_ATP=$(echo $product | jq -r '.minAtp')
      HAS_CRITICAL=$(echo $product | jq -r '.hasCritical')
      
      if [ "$HAS_CRITICAL" = "true" ] || [ $(echo "$MIN_ATP < $ALERT_THRESHOLD" | bc) -eq 1 ]; then
        echo "ALERT: Low stock for ${PRODUCT_NAME} (${SKU}) - ATP: ${MIN_ATP}"
        LOW_STOCK_COUNT=$((LOW_STOCK_COUNT + 1))
      fi
    done
    
    # Check if more pages
    if [ $PAGE -ge $TOTAL_PAGES ]; then
      break
    fi
    PAGE=$((PAGE + 1))
  done
  
  echo "Found ${LOW_STOCK_COUNT} low-stock items"
}

# Run check every 5 minutes
while true; do
  check_atp
  sleep 300  # 5 minutes
done

Complete Example

Here’s a complete Python example for ATP monitoring:
import requests
import time
from datetime import datetime

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://app.betterdata.co/api"
LOCATION_ID = "loc_123"
ALERT_THRESHOLD = 10

def get_atp_matrix(location_id, page=1, limit=100):
    """Fetch ATP matrix for a location."""
    url = f"{BASE_URL}/inventory/channel-location"
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json"
    }
    params = {
        "locationId": location_id,
        "page": page,
        "limit": limit
    }
    
    response = requests.get(url, headers=headers, params=params)
    response.raise_for_status()
    return response.json()

def identify_low_stock(products, threshold):
    """Identify products with low ATP."""
    low_stock = []
    
    for product in products:
        # Check minimum ATP across all channels
        if product["minAtp"] < threshold or product["hasCritical"]:
            low_stock.append({
                "productMasterId": product["productMasterId"],
                "productName": product["productName"],
                "sku": product["globalSku"],
                "minAtp": product["minAtp"],
                "maxAtp": product["maxAtp"],
                "totalOnHand": product["totalOnHand"],
                "totalReserved": product["totalReserved"],
                "channels": [
                    {
                        "channelId": ch["channelId"],
                        "channelName": ch["channelName"],
                        "atp": ch["atp"],
                        "severity": ch["severity"]
                    }
                    for ch in product["channels"]
                    if ch["severity"] in ["LOW", "CRITICAL"]
                ]
            })
    
    return low_stock

def monitor_atp(location_id, threshold, interval_seconds=300):
    """Monitor ATP levels and alert on low stock."""
    print(f"Starting ATP monitoring for location {location_id}")
    print(f"Alert threshold: {threshold}")
    print(f"Check interval: {interval_seconds} seconds")
    print("-" * 50)
    
    while True:
        try:
            # Fetch all pages
            all_products = []
            page = 1
            
            while True:
                data = get_atp_matrix(location_id, page=page)
                all_products.extend(data["products"])
                
                total_pages = data["pagination"]["totalPages"]
                if page >= total_pages:
                    break
                page += 1
            
            # Identify low stock
            low_stock = identify_low_stock(all_products, threshold)
            
            # Report
            timestamp = datetime.now().isoformat()
            print(f"\n[{timestamp}] ATP Check Complete")
            print(f"Total products: {len(all_products)}")
            print(f"Low stock items: {len(low_stock)}")
            
            if low_stock:
                print("\nLow Stock Alerts:")
                for item in low_stock:
                    print(f"  - {item['productName']} ({item['sku']})")
                    print(f"    Min ATP: {item['minAtp']}, On Hand: {item['totalOnHand']}")
                    for channel in item['channels']:
                        print(f"    {channel['channelName']}: ATP={channel['atp']} ({channel['severity']})")
            
            # Wait before next check
            time.sleep(interval_seconds)
            
        except Exception as e:
            print(f"Error during ATP check: {e}")
            time.sleep(60)  # Wait 1 minute before retry

if __name__ == "__main__":
    monitor_atp(LOCATION_ID, ALERT_THRESHOLD, interval_seconds=300)

Idempotency Notes

  • ATP Queries: ATP calculations are read-only and idempotent. Multiple requests return the same results for the same point in time.
  • Monitoring Loops: Each monitoring cycle is independent. Running multiple cycles doesn’t affect data.

Pagination Notes

  • Channel-Location ATP: Supports pagination with page and limit (default: 50, max: 200).
  • Inventory Items: Supports pagination with page and limit (default: 20, max: 100).
  • Best Practice: Fetch all pages to get complete inventory picture for monitoring.

Common Issues

ATP Values Seem Incorrect

Symptom: ATP values don’t match expected calculations. Solutions:
  1. Verify channel policies are configured correctly
  2. Check that reservations are included in calculations
  3. Ensure safety stock is accounted for
  4. Review inbound inventory inclusion rules

Missing Products in ATP Matrix

Symptom: Some products don’t appear in ATP matrix. Solutions:
  1. Verify products have inventory at the location
  2. Check that products are active
  3. Ensure location ID is correct
  4. Review pagination - products may be on other pages

Severity Not Updating

Symptom: Severity remains “OK” despite low ATP. Solutions:
  1. Check channel policy thresholds are set
  2. Verify threshold values are appropriate
  3. Ensure policies are active
  4. Review ATP calculation includes all components


Permissions & Roles

ATP monitoring requires inventory.read permission. No special roles required beyond basic authentication.