Product Insights.
The product-insights endpoint analyzes a single product's sales, purchases, and cost history. It returns a prioritized list of business insights — margin compression, cost spikes, demand anomalies, supplier reliability problems, seasonality, currency effects, and more. Each insight includes a severity, a plain-language explanation, the metrics behind it, and a concrete recommendation. When you call the endpoint again later, it also tells you what has changed since the last analysis.
https://forecastapi.com/v2Bearer YOUR_API_KEYThe Endpoint
The endpoint is a single synchronous POST. Send whichever of the three datasets you have. The
endpoint runs every analyzer that its inputs support and returns all insights in one response. You do not need
to call /forecast or any other endpoint first. The endpoint does the required statistical modeling
internally.
Example Request
{
"identifier": "SKU-12345",
"product_name": "Wireless Mouse",
"base_currency": "USD",
"sales": [
{"date": "2024-01-05", "quantity_sold": 12, "selling_price": 25.00},
{"date": "2024-02-05", "quantity_sold": 18, "selling_price": 25.00},
{"date": "2024-03-05", "quantity_sold": 9, "selling_price": 22.50}
],
"purchases": [
{"date": "2024-01-01", "quantity_purchased": 100, "cost_price": 10.00,
"supplier_id": "SUPP-001", "purchase_order_date": "2023-12-18",
"expected_delivery_date": "2023-12-30", "actual_delivery_date": "2024-01-01"}
],
"cost_history": [
{"date": "2024-01-01", "cost_price": 10.00},
{"date": "2024-03-01", "cost_price": 11.50, "change_reason": "supplier price increase"}
],
"insight_settings": {
"margin_threshold": 0.10,
"enable_seasonality_detection": true
}
}
sales, purchases, cost_history, or any combination. More
datasets enable more insight types (see the catalog).Request Parameters
Top-level fields identify the product and configure the analysis. Unlike the forecasting endpoints,
product-insights takes no model, periods, or frequency.
It works directly from the data you provide.
product_info.name.product_info.group_id.USD). Used when phrasing currency and money-denominated insights."auto" (default) or a number of days between 7 and 365. "auto" derives a sensible window from the span of your data.title / description text. Defaults to "en".identifier across tenants without collisions.sales, purchases, or cost_history must be present and
non-empty. If you send none, the API returns a 422.Datasets
Every row in every dataset needs a date. Dates must be YYYY-MM-DD or
YYYY-MM-DD HH:MM:SS. The format must be consistent within a dataset. You cannot
mix date-only and date-time rows in the same array. The API sorts the rows chronologically for you, so you can
send them in any order.
sales[]
currency.base_currency. When present it is preferred for cross-currency comparisons, so multi-currency insights reflect real economics rather than raw FX.purchases[]
currency.base_currency; preferred for cross-currency comparisons when present.expected_delivery_date is the delivery delay.cost_history[]
recent_cost_change insights.Tuning & Thresholds
The optional insight_settings object controls how sensitive the analyzers are. Thresholds use
fractions: 0.30 means "flag a 30% change." For any value you omit, the API uses the defaults
below.
Response Structure
On success (HTTP 200) the body always has the same top-level shape. It contains a
result object with the analysis and a meta object with timing and data-quality
information.
{
"result": {
"tenant_context": null,
"identifier": "SKU-12345",
"product_info": { "name": "Wireless Mouse", "group_id": null },
"base_currency": "USD",
"insights": [
{
"class": "margin",
"type": "margin_compression",
"severity": "warning",
"title": "Margin dropped 8.2% versus baseline",
"description": "Average gross margin fell from 58.0% to 49.8% over the recent period.",
"metrics": {
"current_margin_percent": 49.8,
"baseline_margin_percent": 58.0,
"margin_change_percentage": -8.2,
"transactions_analyzed": 3
},
"recommendation": {
"action": "review_pricing",
"description": "Review selling price or negotiate cost to protect margin.",
"priority": "high"
},
"confidence": "medium",
"date": "2024-03-05",
"change_status": "new"
}
],
"summary": {
"total_insights": 1,
"severity_distribution": { "critical": 0, "warning": 1, "info": 0 },
"categories": ["profitability"],
"analysis_period": { "start_date": "2024-01-01", "end_date": "2024-03-05", "total_days": 65 },
"data_sources_analyzed": ["sales", "purchases", "cost_history"]
},
"comparison": null
},
"meta": {
"timing": { "validation": 2.1, "analysis": 148.7, "total": 151.9 },
"data_quality": {
"total_data_points": 6,
"datasets_provided": ["sales", "purchases", "cost_history"]
}
}
}
result fields
null if not sent).{ name, group_id } — echoed from the request; either may be null.null on the first-ever analysis of this product; otherwise a diff against the previous run. See Change Tracking.result.summary fields
insights.{ critical, warning, info }. Always present, zeros included.{ start_date, end_date, total_days } spanning all datasets. Dates are null and total_days is 0 if no dates were parseable.["sales","purchases","cost_history"].summary.categories — possible values
Each insight type maps to one business category. On this single-product endpoint you will see these values:
salesprofitabilitycostspurchasingcurrencysales_patternssupply_chainseasonal_patterns
Portfolio-level categories (pricing, customer_risk, portfolio_risk,
categories, inventory_management) only appear via batch
analysis, which compares products against each other.
Anatomy of an Insight
Every entry in insights has the same envelope. The metrics object is the only part
whose keys vary. They depend on the insight type (documented in the catalog).
sales, margin, cost, purchase, currency. Multiple types can share a class.critical, warning, info. Insights are sorted by this, descending.locale.type; numeric values are rounded to 6 decimals.{ action, description, priority } — a suggested next step. priority is one of critical, high, medium, low.high, medium, or low. Sparse history yields lower confidence. Secondary sort key after severity.YYYY-MM-DD), typically the most recent relevant data point.new, changed, unchanged. On a first-ever analysis every insight is new.translated_text.type for display logic. Read severity for prioritization. Treat
metrics as a set of numbers you render on demand. A type can gain new metric keys, and that is
not a breaking change.Insight Catalog
Which analyzers run depends on which datasets you send. The tables below group every insight type
by the dataset that enables it, with the severity levels it can emit and its most important metrics. A type
appears in the response only when it meets its trigger condition, usually a change beyond the relevant
threshold.
Requires sales
| type | Severity | What it flags | Key metrics |
|---|---|---|---|
| sales_volume_anomaly | info → critical | An unusual spike or drop in units sold versus the expected/baseline level, or a shift in demand pattern. | change_percentage, anomaly_type (spike/drop), current_quantity, expected_quantity |
| revenue_change | info → warning | A material change in total revenue between the baseline and recent windows. | baseline_revenue, recent_revenue, revenue_change_percentage |
| margin_compression | warning → critical | Gross margin shrinking versus baseline (needs sales plus a cost signal). | current_margin_percent, baseline_margin_percent, margin_change_percentage |
| low_margin_alert | warning → critical | Margin sitting below a healthy absolute floor, regardless of trend. | current_margin_percent, minimum_threshold, margin_deficit |
| profitability_trend | info → warning | Margin steadily improving or declining over time, or becoming volatile. | trend_direction (improving/declining), margin_change_percentage_points, volatility_level |
| sales_frequency | info → warning | A shift in how often the product sells — e.g. from regular to intermittent demand. | baseline_frequency_days, current_frequency_days, pattern_shift |
| order_size_variability | info → warning | Order sizes becoming more (or less) consistent over time. | baseline_coefficient_variation, current_coefficient_variation, pattern_interpretation |
| seasonality | info | A detected seasonal cycle, an approaching peak, or an entered low season. Requires enough history and enable_seasonality_detection. |
cycle_type (quarterly/annual/…), peak_months, seasonality_strength |
Requires purchases
| type | Severity | What it flags | Key metrics |
|---|---|---|---|
| purchase_pattern_change | info → warning | A change in purchase volume versus the baseline window. | current_avg_quantity, baseline_avg_quantity, volume_change_percentage |
| new_supplier_detected | info → warning | A supplier_id appearing for the first time, with how its cost compares to history. |
new_supplier_id, cost_comparison_percentage, historical_avg_cost |
| supplier_cost_variance | warning | A large cost spread between suppliers for the same product — a sourcing-savings opportunity. | lowest_cost_supplier, highest_cost_supplier, potential_savings |
| purchase_frequency_change | info → warning | A change in how often you reorder. | average_days_between_purchases, recent_days_between_purchases, frequency_change_percentage |
| purchase_cost_volatility | info → warning | Unstable purchase costs across recent orders. | coefficient_of_variation, mean_purchase_cost, volatility_level |
| lead_time_reliability | info → critical | Supplier delivery performance from PO / expected / actual delivery dates. Needs those date fields populated. | average_lead_time_days, on_time_rate_percent, average_delay_days, supplier_metrics |
Requires cost_history
| type | Severity | What it flags | Key metrics |
|---|---|---|---|
| cost_price_change | warning → critical | Average unit cost moving versus baseline, with an estimate of the profitability impact. | current_avg_cost, baseline_avg_cost, cost_change_percentage, profitability_impact |
| recent_cost_change | info → warning | A discrete, recent cost step, echoing your change_reason. |
previous_cost, current_cost, change_date, change_reason |
| cost_volatility | info → warning | Cost fluctuating heavily across the history. | coefficient_of_variation, mean_cost, volatility_level |
Cross-currency (any dataset, needs ≥ 2 distinct currencies)
| type | Severity | What it flags | Key metrics |
|---|---|---|---|
| currency_impact | info → warning | Margin differing across the currencies you sell in, and which currency is weaker/stronger for you. | currency_1, currency_2, margin_difference, weaker_currency |
| currency_pricing_difference | info | The effective selling price (in base currency) differing between currencies. | avg_price_1_base_currency, avg_price_2_base_currency, price_difference_percentage |
| purchase_currency_cost_difference | info | Purchase cost (in base currency) differing between the currencies you buy in. | cost_1, cost_2, difference_percentage, higher_cost_currency |
sales_volume_anomaly. Each method exposes
different metric keys, depending on which one fired. Always read metrics defensively. Check for a key before you
use it, rather than assuming a fixed shape within a type.Change Tracking
The endpoint stores every analysis against its identifier (and tenant_context, if
given). On the next call for the same product, the endpoint compares the new insights against
the previous run and returns a comparison object. You can then show what is new since the last
analysis, instead of showing everything again. The comparison object is null only on
the very first analysis.
"comparison": {
"from": "2024-06-01T09:14:22+00:00",
"to": "2024-07-01T08:02:11+00:00",
"result": [
{
"hasSignificantChange": true,
"summary": "Margin fell a further 4.1 points",
"changes": [
{
"class": "margin",
"metric": "current_margin_percent",
"previousValue": 49.8,
"currentValue": 45.7,
"change": -4.1,
"changePercentage": -8.2,
"isSignificant": true,
"severity": "warning",
"description": "Margin continued to compress."
}
]
}
],
"developments": {
"new": [ { "type": "cost_price_change", "class": "cost", "severity": "warning", "title": "Unit cost up 15%" } ],
"changed": [ { "type": "margin_compression", "class": "margin", "severity": "warning", "title": "Margin dropped further" } ],
"resolved": [ { "type": "sales_volume_anomaly", "class": "sales", "severity": "info", "title": "Sales spike normalised" } ],
"unchanged_count": 2,
"summary": "1 new, 1 changed, 1 resolved, 2 unchanged"
}
}
hasSignificantChange, a summary, and a changes[] list of individual metric movements (with previousValue, currentValue, change, changePercentage).developments
{ type, class, severity, title }."1 new, 1 changed, 1 resolved, 2 unchanged" or "No developments".change_status stamped on each insight in result.insights is the per-insight view
of the same comparison: new / changed / unchanged. resolved
insights are, by definition, no longer in the list. Find them under developments.resolved.Response Metadata
The top-level meta object reports timing and a quick summary of the data you supplied.
["sales","purchases","cost_history"].Batch Analysis
To analyze many products at once, upload them as a batch. Batch analysis runs asynchronously. It also computes portfolio-level insights that compare products against each other: ABC classification, customer and product-group concentration, price elasticity, and weekday performance. The single-product endpoint cannot produce these insights.
Send a series array of 1–1000 items. Each item has exactly the same shape as a single
/product-insights body. The API rejects structural problems immediately with a 422
and a per-index error map, and it queues nothing. Structural problems are a missing identifier, no
dataset at all, or a payload above your plan's datapoint limit. On success you receive 202 Accepted
immediately with a batch you can poll. The full per-datapoint validation runs during processing. An item that
fails it appears as a failed analysis in the batch status, with the validation error, and does not
block the upload.
{
"series": [
{
"identifier": "SKU-1",
"sales": [
{"date": "2024-01-15", "quantity_sold": 10, "selling_price": 5.00},
{"date": "2024-02-15", "quantity_sold": 20, "selling_price": 5.00}
]
},
{
"identifier": "SKU-2",
"cost_history": [
{"date": "2024-01-01", "cost_price": 2.00},
{"date": "2024-03-01", "cost_price": 2.60}
]
}
]
}
identifier.
Very large payloads can exceed the server's request-body limit and fail mid-transfer. For those payloads,
request a presigned upload URL. PUT your JSON payload directly to upload_url. The
payload is an object with the same series array. Then submit the batch: POST the
returned file_key to /v2/batch/product-insights instead of series. With a
file upload, all validation runs asynchronously. Invalid items appear as failed analyses
in the batch status.
{
"upload_url": "https://s3.…(signed URL, valid 30 minutes)",
"headers": {},
"file_key": "batch-uploads/product-insights/42/9b2c….json",
"expires_in": 1800
}
Poll with the batch identifier. The response carries a status of pending,
processing, completed, or failed. When the batch finishes, the response
also carries the per-product results. An unknown identifier returns 404.
{
"uuid": "9b2c...",
"identifier": "my-batch-01",
"status": "completed",
"batch_parts": 1,
"results": [ /* one analysis per product, same shape as result.insights */ ],
"started_at": "2024-07-01T08:00:00+00:00",
"completed_at": "2024-07-01T08:00:12+00:00"
}
Error Responses
HTTP Status Codes
{ error, time_taken_ms }.limit, current_cost, and billing_cycle_end.422 Validation Error
{
"error": "Validation failed",
"errors": {
"identifier": ["The identifier field is required."],
"data": ["At least one dataset (sales, purchases, or cost_history) must be provided."]
},
"time_taken_ms": 1.4
}
400 Bad Request
{
"error": "Could not analyse the supplied data",
"time_taken_ms": 3.2
}
/product-insights call counts as one standard API call. The same
rate limits apply. Your plan caps the total number of data
points you can send per request.