API REFERENCE

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.

Base URL
https://forecastapi.com/v2
Authentication
Bearer YOUR_API_KEY

The 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.

Analyze Product POST /v2/product-insights

Example Request

REQUEST POST /v2/product-insights
{
  "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 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.

identifier STRING · REQUIRED
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 · OPTIONAL
Human-readable name, max 500 chars. Echoed back in product_info.name.
product_group_id STRING · OPTIONAL
Group / category identifier, max 255 chars. Echoed back in product_info.group_id.
base_currency STRING · OPTIONAL
ISO 4217 currency code (exactly 3 chars, e.g. USD). Used when phrasing currency and money-denominated insights.
baseline_period_days STRING | NUMBER · DEFAULT auto
How far back the "baseline" window reaches when comparing recent behavior 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 · OPTIONAL
Sensitivity thresholds and toggles. See Tuning & Thresholds.
locale STRING · DEFAULT en
Language for insight title / description text. Defaults to "en".
tenant_context STRING · OPTIONAL
Multi-tenant scoping tag, max 255 chars. When set, the API scopes the previous-analysis lookup for change tracking to this tenant, so you can reuse the same identifier across tenants without collisions.
WHAT 'CONDITIONAL' MEANS
At least one of 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[]

date STRING · REQUIRED
Transaction date.
quantity_sold NUMBER ≥ 0 · REQUIRED
Units sold in this transaction.
selling_price NUMBER ≥ 0 · REQUIRED
Unit selling price, in the row's currency.
selling_price_converted NUMBER ≥ 0 · OPTIONAL
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) · OPTIONAL
ISO currency code for this row. Two or more distinct currencies across your sales enable the currency insights.
customer_id STRING · OPTIONAL
Customer identifier, max 255 chars.
customer_segment STRING · OPTIONAL
Segment label, max 255 chars.

purchases[]

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

cost_history[]

date STRING · REQUIRED
Date this cost took effect.
cost_price NUMBER ≥ 0 · REQUIRED
Unit cost as of this date.
currency STRING (3) · OPTIONAL
ISO currency code.
change_reason STRING · OPTIONAL
Free-text note, max 500 chars. Surfaced back on 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.

sales_threshold NUMBER · 0.05 – 5.0 · DEFAULT 0.30
Minimum change in sales volume/revenue that triggers a sales insight.
margin_threshold NUMBER · 0.01 – 1.0 · DEFAULT 0.10
Minimum margin movement that triggers a margin / profitability insight.
cost_threshold NUMBER · 0.01 – 1.0 · DEFAULT 0.15
Minimum cost-price change that triggers a cost insight.
purchase_threshold NUMBER · 0.05 – 5.0 · DEFAULT 0.20
Minimum change in purchase volume/pattern that triggers a purchase insight.
enable_seasonality_detection BOOLEAN · DEFAULT true
Whether the seasonality analyzer runs. Needs enough sales history to detect a cycle.
severity_levels STRING[] · critical, warning, info · DEFAULT all three
Reserved for filtering returned insights by severity. Accepted and validated today, but not yet applied — expect all severities back regardless.
LOWER VS. RAISE
Lower the threshold to catch smaller movements and get more, noisier insights. Raise it to surface only large, decision-grade changes.

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.

RESPONSE 200 OK
{
  "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

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

total_insights INTEGER
Number of entries in insights.
severity_distribution OBJECT
Counts per severity: { critical, warning, info }. Always present, zeros included.
categories 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 STRING[]
Which datasets were actually present and analyzed — 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).

class STRING
The analyzer 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, localized 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 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
METRICS VARY BY DETECTION PATH
Several internal methods can produce some types, notably 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"
  }
}
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

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 analyzer'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".
PER-INSIGHT CHANGE STATUS
The 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.

timing.validation NUMBER (MS)
Time spent validating the request.
timing.analysis NUMBER (MS)
Time spent running the analyzers.
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 STRING[]
The non-empty datasets received — subset of ["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.

Upload Batch POST /v2/batch/product-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.

REQUEST POST /v2/batch/product-insights
{
  "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}
      ]
    }
  ]
}
series ARRAY · REQUIRED
1–1000 product bodies, each shaped like a single-product request.
batch STRING · OPTIONAL
Your own batch identifier. If omitted, one is generated and returned as identifier.
File-Based Upload (large payloads) POST /v2/batch/product-insights/presign

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.

RESPONSE POST /v2/batch/product-insights/presign
{
  "upload_url": "https://s3.…(signed URL, valid 30 minutes)",
  "headers": {},
  "file_key": "batch-uploads/product-insights/42/9b2c….json",
  "expires_in": 1800
}
Batch Status GET /v2/batch/product-insights/{batch}

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.

RESPONSE GET /v2/batch/product-insights/{batch}
{
  "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"
}
BILLING
Batch product-insights uses the same billing as other batch operations. See the Batching page for polling patterns and pricing.

Error Responses

HTTP Status Codes

400
Bad Request — A malformed input the analyzer 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

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

RESPONSE 400
{
  "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. Your plan caps the total number of data points you can send per request.

Next Steps

LAST UPDATED — 30 AUG 2026 · FORECASTAPI DOCS
WAS THIS USEFUL? YES NO