Patterns & Segmentation

Some of the most common things people want — a forecast per customer segment, a revenue total split into its parts, a churn rate projection — don't need a special endpoint. This page shows how to build each one with /v2/forecast and /v2/batch/forecast.

Forecast each segment separately

Use tenant_context to split one metric across a dimension. A series is identified by the pair identifier + tenant_context, so mrr on plan-pro and mrr on plan-free are two independent series, each with its own history and its own accuracy tracking.

Despite the name, it isn't only for multi-tenancy. Use it for any dimension you want to forecast along: plan, region, store, channel, cohort, acquisition month, device type.

Batch accepts up to 100,000 series per call on a paid plan (10 on the free tier), so a whole segmentation fits in one request:

curl -X POST "https://forecastapi.com/v2/batch/forecast" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "series": [
      {
        "identifier": "mrr",
        "tenant_context": "plan-pro",
        "data": [{"date": "2024-01-01", "value": 41200}, {"date": "2024-02-01", "value": 43900}]
      },
      {
        "identifier": "mrr",
        "tenant_context": "plan-free",
        "data": [{"date": "2024-01-01", "value": 2100}, {"date": "2024-02-01", "value": 2450}]
      },
      {
        "identifier": "mrr",
        "tenant_context": "plan-enterprise",
        "data": [{"date": "2024-01-01", "value": 88000}, {"date": "2024-02-01", "value": 91500}]
      }
    ],
    "frequency": "M",
    "periods": 6,
    "data_type": "revenue"
  }'

Results come back in results.data, keyed by entity ID rather than by your identifier — the same identifier can appear under several segments in one batch, so it wouldn't be unique. Each result echoes its own identifier and tenant_context, so match on that pair.

Choose your segments before you send history

Each segment builds up its own history and accuracy record. Re-cutting the same data under new tenant_context values later creates new series that start from scratch — it won't relabel the ones you already have.

Break a total into components

To forecast the parts of a total — new, expansion, contraction and churned revenue, or a split by category, channel or region — send each part as its own series and add them up on your side. You get a forecast per component instead of one aggregate number.

{
  "series": [
    {"identifier": "revenue-new",
     "data": [{"date": "2024-01-01", "value": 12000}, {"date": "2024-02-01", "value": 13100}]},
    {"identifier": "revenue-expansion",
     "data": [{"date": "2024-01-01", "value": 4300}, {"date": "2024-02-01", "value": 4650}]},
    {"identifier": "revenue-contraction",
     "data": [{"date": "2024-01-01", "value": -1800}, {"date": "2024-02-01", "value": -1720}]},
    {"identifier": "revenue-churned",
     "data": [{"date": "2024-01-01", "value": -3100}, {"date": "2024-02-01", "value": -2950}]}
  ],
  "frequency": "M",
  "periods": 6,
  "data_type": "revenue"
}

If you also forecast the total as its own series, the components won't add up to it. Each series is fitted independently and picks its own model and trend, so the two numbers will disagree. When the roll-up itself is the deliverable, use grouped forecasting instead — it reconciles the whole hierarchy so the parts sum to the whole by construction. If you stay on batch, pick one published figure — either the sum of the parts or the standalone total — rather than showing both.

Don't add up the confidence bounds

Summing each component's lower value doesn't give you a lower bound for the sum — it assumes every component misses low at the same time, which is far more pessimistic than reality. Add up the point forecasts and treat the total's uncertainty as an open question — or let grouped forecasting build the aggregate band under a stated correlation assumption. For the equivalent problem across periods rather than across series, accumulate handles it explicitly.

Forecast rates and percentages

Churn rate, conversion rate, utilisation and margin are all ordinary time series and forecast normally. Send them in whatever unit you already use — 0–1 or 0–100 — as long as you're consistent.

Tell the API the range your series can't leave with value_bounds. Either side is optional — a rate has both, a backlog only has a floor:

{
  "identifier": "churn-rate",
  "data": [{"date": "2024-01-01", "value": 2.4}, {"date": "2024-02-01", "value": 3.1}],
  "frequency": "M",
  "periods": 12,
  "value_bounds": {"min": 0, "max": 100}
}

The series is then forecast on a transformed scale and mapped back, so the forecast, both confidence bounds and every quantile land inside the range. The band also stops being symmetric: on a rate sitting near zero it has more room above than below, which is the honest shape for a number that can't go negative.

Why not just clamp the bounds yourself?

Because clamping cuts off probability without redistributing it, so a clamped band no longer covers at its stated confidence level — you asked for 80% and got less. It also can't fix the shape of the band inside the range, only chop off the ends. value_bounds changes what's forecast rather than what's displayed, so the stated coverage still holds.

Bounds have to match your data

If the history you send already falls outside the range you declared, the bounds are reported back as not applied and the series is forecast normally — rather than squeezing real observations into a range they contradict. Check model_info.bounds_transform in the response, which says whether the transform ran and why not if it didn't. The usual cause is a unit mismatch: sending 0–1 rates while declaring {"min": 0, "max": 100} works, but the reverse doesn't.

Build a fan chart

A fan chart needs several quantiles per period. Request them together with the quantiles parameter and each forecast row comes back with a quantiles object alongside the usual bounds:

{
  "identifier": "SKU-12345",
  "data": [{"date": "2024-01-01", "value": 120}, {"date": "2024-02-01", "value": 135}],
  "frequency": "M",
  "periods": 6,
  "quantiles": [0.1, 0.3, 0.5, 0.7, 0.9]
}

Why not call the same series at several confidence levels?

It's the intuitive approach, but it costs more and gives you less. Results aren't cached, so five confidence levels bill as five forecasts.

More importantly, 0.80 is the widest band the foundation models produce natively. advanced-quantized, advanced-patched and ensemble return the same 80% band whether you ask for 0.85 or 0.95, so those lines land on top of each other. One quantiles request gives you genuinely distinct levels, read from each model's own quantile heads, for a single charge.

What's not directly supported

Per-customer churn scoring and per-customer LTV. Answering "which of these customers will churn, and what is each one worth" means classifying customers by their attributes — plan, tenure, usage, support history — which is a different kind of model from time series forecasting, and needs data this API doesn't take in.

The aggregate version does work well: forecast a churn rate or revenue series per cohort with tenant_context, then use accumulate to turn that forecast into a discounted lifetime total. That tells you what a segment is worth, rather than what one customer is worth.

Next Steps