Product Insights

The product-insights endpoint analyses a single product's sales, purchases, and cost history and returns a prioritised list of business insights — margin compression, cost spikes, demand anomalies, supplier reliability problems, seasonality, currency effects, and more. Each insight comes with a severity, a plain-language explanation, the metrics behind it, and a concrete recommendation. Call it again later and it also tells you what has changed since the last time.

Base URL

https://forecastapi.com/v2

Authentication

Bearer YOUR_API_KEY

1. The Endpoint

A single synchronous POST. Send whichever of the three datasets you have; the endpoint runs every analyser that its inputs support and returns all insights in one response. You do not need to call /forecast or any other endpoint first — the required statistical modelling happens internally.

Analyse Product

POST /product-insights

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
  }
}

At least one dataset is required. You may send sales, purchases, cost_history, or any combination. More datasets unlock more insight types (see the catalog).

2. 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 off the data you provide.

Parameter Type Required Description
identifier string Yes Unique product identifier (SKU or product ID), max 255 chars. Echoed back, and used to match this product against its previous analysis for change tracking.
sales array Conditional Historical sales transactions. See Datasets.
purchases array Conditional Historical purchase orders / receipts. See Datasets.
cost_history array Conditional A time series of unit cost changes. See Datasets.
product_name string No Human-readable name, max 500 chars. Echoed back in product_info.name.
product_group_id string No Group / category identifier, max 255 chars. Echoed back in product_info.group_id.
base_currency string No ISO 4217 currency code (exactly 3 chars, e.g. USD). Used when phrasing currency and money-denominated insights.
baseline_period_days string | number No How far back the "baseline" window reaches when comparing recent behaviour to normal. Either the string "auto" (default) or a number of days between 7 and 365. "auto" derives a sensible window from the span of your data.
insight_settings object No Sensitivity thresholds and toggles. See Tuning & Thresholds.
locale string No Language for insight title / description text. Defaults to "en".
tenant_context string No Multi-tenant scoping tag, max 255 chars. When set, the previous-analysis lookup for change tracking is scoped to this tenant, so the same identifier can be reused across tenants without collisions.

"Conditional" means at least one of sales, purchases, or cost_history must be present and non-empty. Sending none returns a 422.

3. Datasets

Every row in every dataset needs a date. Dates must be YYYY-MM-DD or YYYY-MM-DD HH:MM:SS, and the format must be consistent within a dataset (you cannot mix date-only and date-time rows in the same array). Rows are sorted chronologically for you, so you can send them in any order.

sales[]

Field Type Required Description
date string Yes Transaction date.
quantity_sold number ≥ 0 Yes Units sold in this transaction.
selling_price number ≥ 0 Yes Unit selling price, in the row's currency.
selling_price_converted number ≥ 0 No Unit selling price pre-converted to your base_currency. When present it is preferred for cross-currency comparisons, so multi-currency insights reflect real economics rather than raw FX.
currency string (3) No ISO currency code for this row. Two or more distinct currencies across your sales unlock the currency insights.
customer_id string No Customer identifier, max 255 chars.
customer_segment string No Segment label, max 255 chars.

purchases[]

Field Type Required Description
date string Yes Purchase / receipt date.
quantity_purchased number ≥ 0 Yes Units purchased.
cost_price number ≥ 0 Yes Unit cost, in the row's currency.
cost_price_converted number ≥ 0 No Unit cost pre-converted to base_currency; preferred for cross-currency comparisons when present.
currency string (3) No ISO currency code for this purchase.
supplier_id string No Supplier identifier, max 255 chars. Enables new-supplier detection and supplier cost-variance insights.
purchase_order_date date No When the PO was raised. Together with the two delivery dates below, drives the lead-time reliability insight.
expected_delivery_date date No Promised delivery date.
actual_delivery_date date No When goods actually arrived. The gap versus expected_delivery_date is the delivery delay.

cost_history[]

Field Type Required Description
date string Yes Date this cost took effect.
cost_price number ≥ 0 Yes Unit cost as of this date.
currency string (3) No ISO currency code.
change_reason string No Free-text note, max 500 chars. Surfaced back on recent_cost_change insights.

4. Tuning & Thresholds

The optional insight_settings object controls how sensitive the analysers are. Thresholds are expressed as fractions — 0.30 means "flag a 30% change." Anything you omit falls back to the defaults below.

Field Type Range Default Controls
sales_threshold number 0.05 – 5.0 0.30 Minimum change in sales volume/revenue that triggers a sales insight.
margin_threshold number 0.01 – 1.0 0.10 Minimum margin movement that triggers a margin / profitability insight.
cost_threshold number 0.01 – 1.0 0.15 Minimum cost-price change that triggers a cost insight.
purchase_threshold number 0.05 – 5.0 0.20 Minimum change in purchase volume/pattern that triggers a purchase insight.
enable_seasonality_detection boolean true / false true Whether the seasonality analyser runs. Needs enough sales history to detect a cycle.
severity_levels array<string> critical, warning, info all three Reserved for filtering returned insights by severity. Accepted and validated today, but not yet applied — expect all severities back regardless.

Lower the threshold to catch smaller movements (more, noisier insights); raise it to surface only large, decision-grade changes.

5. Response Structure

On success (HTTP 200) the body always has the same top-level shape: a result object holding the analysis, and a meta object holding timing and data-quality info.

{
  "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

Field Type Description
identifier string The product identifier, echoed from the request.
tenant_context string | null The tenant tag, echoed from the request (null if not sent).
product_info object { name, group_id } — echoed from the request; either may be null.
base_currency string | null Echoed from the request.
insights array The insights, sorted most-urgent first (by severity, then confidence). May be empty if nothing crossed a threshold. See Anatomy of an Insight.
summary object Roll-up counts and metadata about this analysis (see below).
comparison object | null null on the first-ever analysis of this product; otherwise a diff against the previous run. See Change Tracking.

result.summary fields

Field Type Description
total_insights integer Number of entries in insights.
severity_distribution object Counts per severity: { critical, warning, info }. Always present, zeros included.
categories array<string> Distinct business categories represented in the insights (see the value list below).
analysis_period object { start_date, end_date, total_days } spanning all datasets. Dates are null and total_days is 0 if no dates were parseable.
data_sources_analyzed array<string> Which datasets were actually present and analysed — subset of ["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:

sales profitability costs purchasing currency sales_patterns supply_chain seasonal_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).

Field Type Description
class string The analyser family that produced it — e.g. sales, margin, cost, purchase, currency. Multiple types can share a class.
type string The specific insight, e.g. margin_compression. The full set is in the catalog.
severity string How urgent: critical warning info. Insights are sorted by this, descending.
title string Short headline, localised to locale.
description string One or two sentences explaining what was detected.
metrics object The numbers behind the insight. Keys depend on type; numeric values are rounded to 6 decimals.
recommendation object { action, description, priority } — a suggested next step. priority is one of critical, high, medium, low.
confidence string How much data backs the insight: high, medium, or low. Sparse history yields lower confidence. Secondary sort key after severity.
date string The date the insight is anchored to (YYYY-MM-DD), typically the most recent relevant data point.
change_status string How this insight compares to the previous analysis: new changed unchanged. On a first-ever analysis every insight is new.
translation_key string Optional. Present on a few types (e.g. sales anomalies) to support client-side re-translation. Accompanied by translated_text.

Consuming insights generically: switch on type for display logic, read severity for prioritisation, and treat metrics as a bag of numbers you render on demand — new metric keys can be added within a type without being a breaking change.

6. Insight Catalog

Which analysers run depends on which datasets you send. The tables below group every insight type by the dataset that unlocks it, with the severity levels it can emit and its most important metrics. A type only appears in the response when its trigger condition (usually a change beyond the relevant threshold) is met.

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

Metrics vary by detection path. Some types (notably sales_volume_anomaly) can be produced by several internal methods and expose different metric keys depending on which fired. Always read metrics defensively — check for a key before using it rather than assuming a fixed shape within a type.

7. Change Tracking

Every analysis is stored against its identifier (and tenant_context, if given). On the next call for the same product, the endpoint diffs the new insights against the previous run and returns a comparison object — so you can surface "what's new since last time" instead of re-showing everything. It 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"
  }
}
Field Type Description
from datetime ISO 8601 timestamp of the previous analysis.
to datetime ISO 8601 timestamp of this analysis.
result array Per-type comparisons that carried a significant change. Each has hasSignificantChange, a summary, and a changes[] list of individual metric movements (with previousValue, currentValue, change, changePercentage).
developments object A new / changed / resolved breakdown for quick scanning (see below).

developments

Field Type Meaning
new array Insight types present now but absent last time. Each item is a compact { type, class, severity, title }.
changed array Types present in both runs whose underlying metrics moved significantly (using each analyser's own significance logic).
resolved array Types present last time but gone now — the situation cleared up.
unchanged_count integer How many current insights were essentially the same as before.
summary string One-line roll-up, e.g. "1 new, 1 changed, 1 resolved, 2 unchanged" or "No developments".

The change_status stamped on each insight in result.insights is the per-insight view of the same diff: new / changed / unchanged. (resolved insights are, by definition, no longer in the list — find them under developments.resolved.)

8. Response Metadata

The top-level meta object reports timing and a quick read on the data you supplied.

Field Type Description
timing.validation number (ms) Time spent validating the request.
timing.analysis number (ms) Time spent running the analysers.
timing.total number (ms) End-to-end server time for the request.
data_quality.total_data_points integer Combined row count across all datasets you sent.
data_quality.datasets_provided array<string> The non-empty datasets received — subset of ["sales","purchases","cost_history"].

9. Batch Analysis

To analyse many products at once, upload them as a batch. Batch analysis runs asynchronously and additionally computes portfolio-level insights that compare products against each other (ABC classification, customer and product-group concentration, price elasticity, weekday performance) — insights the single-product endpoint cannot produce.

Upload Batch

POST /batch/product-insights

Send a series array of 1–1000 items, where each item is exactly the same shape as a single /product-insights body. Every item is validated up front; if any fail you get a 422 with a per-index error map and nothing is queued. On success you get 202 Accepted with a batch you can poll.

{
  "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}
      ]
    }
  ]
}
Field Type Required Description
series array Yes 1–1000 product bodies, each shaped like a single-product request.
batch string No Your own batch identifier. If omitted, one is generated and returned as identifier.

Batch Status

GET /batch/product-insights/{batch}

Poll with the batch identifier. The response carries a status of pending, processing, completed, or failed, plus the per-product results once finished. 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"
}

Batch product-insights is billed like other batch operations. See the Batching page for polling patterns and pricing.

10. Error Responses

HTTP Status Codes

400
Bad Request
A malformed input the analyser rejected. Body: { error, time_taken_ms }.
401
Unauthorized
Invalid or missing API key.
402
Payment Required
Your monthly spending limit has been reached. Body includes limit, current_cost, and billing_cycle_end.
403
Forbidden
The token is not permitted to use the API.
422
Unprocessable Entity
Validation failed — no dataset provided, a bad or inconsistent date format, an out-of-range threshold, or too many data points for your plan. The body lists per-field errors.
429
Too Many Requests
You have exceeded your plan's request allowance. See Rate Limiting.

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
}

Usage & Cost

Each /product-insights call counts as one standard API call. The same rate limits apply, and the total number of data points you can send per request is capped by your plan.

Next Steps