Guides

ATP Monitoring and Low-Stock Alerts

Edit this page

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:

1curl -X GET "https://app.betterdata.co/api/inventory/channel-location?locationId=loc_123&page=1&limit=100" \
2 -H "Authorization: Bearer YOUR_API_KEY" \
3 -H "Content-Type: application/json"

Response:

1{
2 "products": [
3 {
4 "productMasterId": "prod_123",
5 "productName": "Product Name",
6 "globalSku": "SKU-123",
7 "channels": [
8 {
9 "channelId": "DTC",
10 "channelName": "Direct to Consumer",
11 "atp": 150,
12 "onHand": 200,
13 "reserved": 50,
14 "available": 150,
15 "severity": "OK",
16 "threshold": 10,
17 "policyId": "policy_123"
18 },
19 {
20 "channelId": "WHOLESALE",
21 "channelName": "Wholesale",
22 "atp": 5,
23 "onHand": 20,
24 "reserved": 15,
25 "available": 5,
26 "severity": "LOW",
27 "threshold": 10,
28 "policyId": "policy_456"
29 }
30 ],
31 "totalOnHand": 220,
32 "totalReserved": 65,
33 "minAtp": 5,
34 "maxAtp": 150,
35 "hasCritical": false
36 }
37 ],
38 "channels": [
39 {
40 "channelId": "DTC",
41 "channelName": "Direct to Consumer",
42 "channelType": "E_COMMERCE",
43 "policyId": "policy_123",
44 "lowAtpThreshold": 10
45 }
46 ],
47 "location": {
48 "id": "loc_123",
49 "name": "Main Warehouse"
50 },
51 "pagination": {
52 "page": 1,
53 "pageSize": 100,
54 "total": 500,
55 "totalPages": 5
56 }
57}

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:

1function identifyLowStock(products) {
2 const lowStock = [];
3
4 for (const product of products) {
5 "cmt">// Check each channel
6 for (const channel of product.channels) {
7 if (channel.severity === 'LOW' || channel.severity === 'CRITICAL') {
8 lowStock.push({
9 productMasterId: product.productMasterId,
10 productName: product.productName,
11 sku: product.globalSku,
12 channelId: channel.channelId,
13 channelName: channel.channelName,
14 atp: channel.atp,
15 threshold: channel.threshold,
16 severity: channel.severity,
17 onHand: channel.onHand,
18 reserved: channel.reserved
19 });
20 }
21 }
22
23 "cmt">// Also check overall minimum ATP
24 if (product.minAtp <= 0) {
25 lowStock.push({
26 productMasterId: product.productMasterId,
27 productName: product.productName,
28 sku: product.globalSku,
29 channelId: 'ALL',
30 channelName: 'All Channels',
31 atp: product.minAtp,
32 severity: 'CRITICAL',
33 onHand: product.totalOnHand,
34 reserved: product.totalReserved
35 });
36 }
37 }
38
39 return lowStock;
40}

Step 3: Get Detailed Inventory Levels

For low-stock items, get detailed inventory information.

Request:

1curl -X GET "https://app.betterdata.co/api/inventory?locationId=loc_123&productMasterId=prod_123" \
2 -H "Authorization: Bearer YOUR_API_KEY" \
3 -H "Content-Type: application/json"

Response:

1{
2 "items": [
3 {
4 "id": "item_123",
5 "productMasterId": "prod_123",
6 "locationId": "loc_123",
7 "quantityOnHand": 200,
8 "quantityReserved": 50,
9 "quantityAvailable": 150,
10 "lotId": "lot_123",
11 "expiryDate": "2024-12-31",
12 "binId": "bin_123"
13 }
14 ],
15 "pagination": {
16 "page": 1,
17 "pageSize": 20,
18 "total": 1,
19 "totalPages": 1
20 }
21}

Step 4: Set Up Monitoring Loop

Create a monitoring script that periodically checks ATP levels.

Example Script:

1#!/bin/bash
2 
3API_KEY="YOUR_API_KEY"
4BASE_URL="https://app.betterdata.co/api"
5LOCATION_ID="loc_123"
6ALERT_THRESHOLD=10 # Alert if ATP < 10
7 
8# Function to check ATP and alert on low stock
9check_atp() {
10 PAGE=1
11 LOW_STOCK_COUNT=0
12
13 while true; do
14 RESPONSE=$(curl -s -X GET "${BASE_URL}/inventory/channel-location?locationId=${LOCATION_ID}&page=${PAGE}&limit=100" \
15 -H "Authorization: Bearer ${API_KEY}" \
16 -H "Content-Type: application/json")
17
18 PRODUCTS=$(echo $RESPONSE | jq -r '.products[]')
19 TOTAL_PAGES=$(echo $RESPONSE | jq -r '.pagination.totalPages')
20
21 # Process each product
22 echo $PRODUCTS | jq -c '.' | while read product; do
23 PRODUCT_NAME=$(echo $product | jq -r '.productName')
24 SKU=$(echo $product | jq -r '.globalSku')
25 MIN_ATP=$(echo $product | jq -r '.minAtp')
26 HAS_CRITICAL=$(echo $product | jq -r '.hasCritical')
27
28 if [ "$HAS_CRITICAL" = "true" ] || [ $(echo "$MIN_ATP < $ALERT_THRESHOLD" | bc) -eq 1 ]; then
29 echo "ALERT: Low stock for ${PRODUCT_NAME} (${SKU}) - ATP: ${MIN_ATP}"
30 LOW_STOCK_COUNT=$((LOW_STOCK_COUNT + 1))
31 fi
32 done
33
34 # Check if more pages
35 if [ $PAGE -ge $TOTAL_PAGES ]; then
36 break
37 fi
38 PAGE=$((PAGE + 1))
39 done
40
41 echo "Found ${LOW_STOCK_COUNT} low-stock items"
42}
43 
44# Run check every 5 minutes
45while true; do
46 check_atp
47 sleep 300 # 5 minutes
48done

Complete Example

Here's a complete Python example for ATP monitoring:

1import requests
2import time
3from datetime import datetime
4 
5API_KEY = "YOUR_API_KEY"
6BASE_URL = "https://app.betterdata.co/api"
7LOCATION_ID = "loc_123"
8ALERT_THRESHOLD = 10
9 
10def get_atp_matrix(location_id, page=1, limit=100):
11 """Fetch ATP matrix for a location."""
12 url = f"{BASE_URL}/inventory/channel-location"
13 headers = {
14 "Authorization": f"Bearer {API_KEY}",
15 "Content-Type": "application/json"
16 }
17 params = {
18 "locationId": location_id,
19 "page": page,
20 "limit": limit
21 }
22
23 response = requests.get(url, headers=headers, params=params)
24 response.raise_for_status()
25 return response.json()
26 
27def identify_low_stock(products, threshold):
28 """Identify products with low ATP."""
29 low_stock = []
30
31 for product in products:
32 # Check minimum ATP across all channels
33 if product["minAtp"] < threshold or product["hasCritical"]:
34 low_stock.append({
35 "productMasterId": product["productMasterId"],
36 "productName": product["productName"],
37 "sku": product["globalSku"],
38 "minAtp": product["minAtp"],
39 "maxAtp": product["maxAtp"],
40 "totalOnHand": product["totalOnHand"],
41 "totalReserved": product["totalReserved"],
42 "channels": [
43 {
44 "channelId": ch["channelId"],
45 "channelName": ch["channelName"],
46 "atp": ch["atp"],
47 "severity": ch["severity"]
48 }
49 for ch in product["channels"]
50 if ch["severity"] in ["LOW", "CRITICAL"]
51 ]
52 })
53
54 return low_stock
55 
56def monitor_atp(location_id, threshold, interval_seconds=300):
57 """Monitor ATP levels and alert on low stock."""
58 print(f"Starting ATP monitoring for location {location_id}")
59 print(f"Alert threshold: {threshold}")
60 print(f"Check interval: {interval_seconds} seconds")
61 print("-" * 50)
62
63 while True:
64 try:
65 # Fetch all pages
66 all_products = []
67 page = 1
68
69 while True:
70 data = get_atp_matrix(location_id, page=page)
71 all_products.extend(data["products"])
72
73 total_pages = data["pagination"]["totalPages"]
74 if page >= total_pages:
75 break
76 page += 1
77
78 # Identify low stock
79 low_stock = identify_low_stock(all_products, threshold)
80
81 # Report
82 timestamp = datetime.now().isoformat()
83 print(f"\n[{timestamp}] ATP Check Complete")
84 print(f"Total products: {len(all_products)}")
85 print(f"Low stock items: {len(low_stock)}")
86
87 if low_stock:
88 print("\nLow Stock Alerts:")
89 for item in low_stock:
90 print(f" - {item['productName']} ({item['sku']})")
91 print(f" Min ATP: {item['minAtp']}, On Hand: {item['totalOnHand']}")
92 for channel in item['channels']:
93 print(f" {channel['channelName']}: ATP={channel['atp']} ({channel['severity']})")
94
95 # Wait before next check
96 time.sleep(interval_seconds)
97
98 except Exception as e:
99 print(f"Error during ATP check: {e}")
100 time.sleep(60) # Wait 1 minute before retry
101 
102if __name__ == "__main__":
103 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.