About Services Blog Contact us
← Back to Blog 31 March 2026

Stock Sell-Through & Prediction Reports Using the Brightpearl API

Brightpearl

Brightpearl holds a wealth of inventory data, but out of the box it doesn’t answer the questions that keep stock managers awake: how fast is each SKU actually selling, and when will it run out? In a recent client project we built a bespoke reporting layer on top of the Brightpearl API that answers both — automatically. Here’s how we approached it.

The Endpoints You Need

Brightpearl’s REST API is split across several services. For sell-through and prediction reporting, four areas are essential:

Endpoint Service Purpose
/warehouse-service/product-availability/{ids} Warehouse Current on-hand stock per product & warehouse
/warehouse-service/goods-movement-search Warehouse Historical goods-in (GI), goods-out (GO), stock corrections (SC) & internal transfers (IT)
/warehouse-service/order/{ids}/goods-note/goods-in Warehouse Goods-in notes against specific purchase orders
/order-service/order-search Order Purchase order lookup (filter orderTypeId = 2)
/order-service/order/{ids} Order Full order detail including rows, dates & delivery status
/product-service/product-search Product SKU metadata, filtering by prefix, batch retrieval

All calls share the same base URL pattern: https://ws-{datacentre}.brightpearl.com/public-api/{account}/ with a Bearer token and brightpearl-app-ref header. We recommend wrapping every call through a single API client class that handles token refresh and Brightpearl’s 150–200 requests-per-minute throttle.

Calculating Sell-Through Rate

The classic retail definition of sell-through — units sold ÷ units received × 100 — needs a concrete data source when you’re computing it from Brightpearl. We derive it like this:

  1. Pull the receipt history. Query goods-movement-search filtered by product and movement types GI, SC, and IT. Aggregate quantities by date to identify each receipt event.
  2. Identify the last received batch. Take the most recent receipt day and its total quantity (lastReceivedQty). If a goods-in note exists against a purchase order, prefer that — it’s the most accurate source.
  3. Read current stock. Call product-availability/{ids} to get inStock.
  4. Compute implied sales. qtySold = max(0, lastReceivedQty − currentStock)
  5. Sell-through %. percentSold = (qtySold / lastReceivedQty) × 100

In PHP, the core of that looks like this:

$qtySold = max(0, $lastReceivedQty - $currentStock);
$sellThrough = $lastReceivedQty > 0
? round(($qtySold / $lastReceivedQty) * 100, 1)
: 0;

For a dashboard-level aggregate, sum across all SKUs in a product group:

$avgSellThrough = $totalReceived > 0
? round(($totalSold / $totalReceived) * 100, 1)
: 0;

This approach is especially useful for retail and wholesale businesses that receive stock in defined batches — it tells you, at a glance, what proportion of the latest delivery has already moved.

Predicting Sell-Out Dates

Once you have a sell-through rate, projecting a sell-out date is straightforward:

  1. Calculate a daily run-rate. dailyRate = qtySold / daysSinceLastReceipt (clamped to a minimum of 1 day to avoid division by zero).
  2. Fallback to previous period. If the latest receipt is too recent for a meaningful rate, look at the previous receipt and compute sales between the two receipt dates. This gives a longer baseline.
  3. Project the date. daysToSellout = ceil(currentStock / dailyRate), then add that to today’s date.
$daysSince = max((time() - strtotime($lastReceivedOn)) / 86400, 1);
$dailyRate = $qtySold / $daysSince;

if ($dailyRate > 0 && $currentStock > 0) {
$daysToSellout = ceil($currentStock / $dailyRate);
$projectedDate = date('Y-m-d', strtotime("+{$daysToSellout} days"));
}

The beauty of this heuristic is its simplicity — no ML pipeline, no training data, just clean inventory maths that updates every time you refresh the report. For most mid-market retailers, this is more than sufficient to catch impending stock-outs before they become lost sales.

Smart Reorder Alerts

A sell-out projection becomes genuinely powerful when you combine it with supplier lead times. We maintain a lead_times table keyed by SKU prefix, populated automatically by analysing historical purchase orders:

  1. Scan POs from the last 24 months via order-search with orderTypeId = 2.
  2. For each PO, fetch goods-in notes and compute leadTimeDays = receivedDate − orderPlacedDate.
  3. Average by SKU prefix to produce a reliable per-supplier figure.

With that in place, a reorder is flagged as urgent when:

$reorderUrgent = (strtotime($projectedSellout)
<= strtotime("+{$leadTimeDays} days +3 days"));

The three-day buffer accounts for order processing time. On the dashboard, urgent items surface in red — giving buyers a clear, prioritised action list rather than a sea of spreadsheet rows.

Dead & Slow Stock Detection

The same goods-movement data powers a dead stock report. By filtering goods-movement-search for GO (goods-out) movements, you can identify the last sale date for every SKU and classify them:

  • Healthy — last sale within a third of your threshold (e.g. under 30 days for a 90-day threshold)
  • Slow — last sale within the threshold but beyond the healthy window
  • Dead — no sale for longer than the threshold, stock still on hand

A companion recently sold out view catches SKUs where inStock = 0 but a goods-out movement occurred in the last 30 days — useful for spotting lines that need urgent replenishment before customers notice.

Purchase Order Tracking

To close the loop, we pull open purchase orders and cross-reference goods-in totals against ordered quantities. Each PO row gets a fulfilment status, and the overall order is flagged as:

  • Overdue — past the expected delivery date with outstanding items
  • Due soon — delivery expected within 7 days
  • Fully received — all goods-in notes match ordered quantities

This turns Brightpearl’s purchase order data into a live supplier performance tracker — without leaving your bespoke reporting dashboard.

Performance Considerations

Brightpearl’s API rate limit of roughly 150 requests per minute means you can’t afford to hit it on every page load. Our approach:

  • Batch product IDs. The product-availability endpoint accepts comma-separated IDs (we batch in groups of 25).
  • Cache aggressively. Report results are stored in a MySQL report_cache table with a 6-hour TTL, keyed by report type and SKU prefix. A ?refresh=1 parameter forces a fresh pull.
  • Throttle in the client. Our API wrapper tracks call counts per minute and sleeps when approaching the limit — preventing 429 errors from cascading through a large report.
// Simplified cache check
$cacheKey = "sell_through_{$skuPrefix}";
$cached = $db->query(
"SELECT data FROM report_cache
WHERE cache_key = ? AND created_at > NOW() - INTERVAL 6 HOUR",
[$cacheKey]
)->fetch();

if ($cached) {
return json_decode($cached['data'], true);
}

Putting It All Together

With these building blocks — sell-through rates, projected sell-out dates, reorder urgency, dead stock flags, and PO tracking — you end up with a single dashboard that turns Brightpearl’s transactional data into actionable intelligence. Stock managers can see at a glance which lines are flying, which are gathering dust, and which need reordering today.

The Brightpearl API documentation is available at api-docs.brightpearl.com. If you’re looking to build something similar for your inventory operation, get in touch — we’d be happy to help.