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.
- 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:
- Verify channel policies are configured correctly
- Check that reservations are included in calculations
- Ensure safety stock is accounted for
- Review inbound inventory inclusion rules
Missing Products in ATP Matrix
Symptom: Some products don’t appear in ATP matrix.
Solutions:
- Verify products have inventory at the location
- Check that products are active
- Ensure location ID is correct
- Review pagination - products may be on other pages
Severity Not Updating
Symptom: Severity remains “OK” despite low ATP.
Solutions:
- Check channel policy thresholds are set
- Verify threshold values are appropriate
- Ensure policies are active
- Review ATP calculation includes all components
Related Pages
Permissions & Roles
ATP monitoring requires inventory.read permission. No special roles required beyond basic authentication.