Forecasts
A forecast is a model-generated projection of a monthly time series. Results include point estimates with quantile bands, per-driver attributions, and optional rolling-window backtest metrics. The Sybilion pipeline selects the most relevant macroeconomic signals, regional and category dimensions, and fits the best model for the series.
Forecast jobs are asynchronous. Submitting a request returns a job_id immediately while the pipeline runs in the background, typically finishing within a few minutes. Poll the job status until it is completed, then download the resulting artifact files.
The full process:
- Submit
POST /api/v1/forecastswith the time series and metadata to receive ajob_id. - Poll
GET /api/v1/forecasts/{id}untilstatus: "completed". - Download the artifact files listed in the completed job response.
In this page, code examples are shown for curl, the Python SDK, and the Go SDK. For full validation rules and field-level reference, see Forecast submission, Forecast status, and Artifact download.
Usa cases
- Forecasting a monthly series of 40+ observations (more for longer horizons, see below).
- Getting point forecasts with optional quantile bands over 1 to 12 months.
- Understanding which external drivers impact the series and by how much.
- Validating forecast quality with rolling backtest metrics.
To get driver recommendations synchronously without running a full forecast, use Drivers instead.
Prepare data
The timeseries is submitted as a JSON object where each key is a date and the value is a numeric observation. Keys must follow the format YYYY-MM-DD and must be the first day of the month — any other day-of-month is rejected. The most recent observation must fall within the past 12 months. The minimum number of observations depends on the forecast horizon (soft_horizon or hard_horizon, whichever is larger):
| Horizon (months) | Minimum observations |
|---|---|
| 1–3 | 40 |
| 4–6 | 60 |
| 7–12 | 120 |
We recommend storing the full request body in a JSON file. The file structure looks like this :
{
"pipeline_version": "v1",
"frequency": "monthly",
"recency_factor": 0.6,
"soft_horizon": 6,
"backtest": true,
"timeseries_metadata": {
"title": "Brent Crude Oil Price Monthly",
"description": "Monthly average Brent crude oil spot price in USD/barrel, sourced from EIA.",
"keywords": ["oil", "brent", "energy", "commodity"]
},
"timeseries": {
"2021-01-01": 57.64,
"2021-02-01": 65.02,
"2021-03-01": 67.24,
"...": "...",
"2025-10-01": 91.05,
"2025-11-01": 81.77,
"2025-12-01": 76.10
}
}Save this as forecast_body.json — the examples in the next step reference it by filename.
Submit forecast job
Required fields: pipeline_version, frequency, recency_factor, timeseries_metadata, timeseries, and at least one of soft_horizon or hard_horizon.
curl -sS -X POST https://api.sybilion.dev/api/v1/forecasts \
-H "Authorization: Bearer $SYBILION_API_TOKEN" \
-H "Content-Type: application/json" \
-d @forecast_body.jsonimport json
import os
from sybilion import Client
client = Client(token=os.environ["SYBILION_API_TOKEN"])
with open("forecast_body.json", encoding="utf-8") as f:
body = json.load(f)
submit = client._api.api_v1_forecasts_post(forecast_request_v1=body)
print("job_id:", submit.job_id)package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"go.sybilion.dev/sybilion"
api "go.sybilion.dev/sybilion/api"
)
func main() {
c := sybilion.New(sybilion.Options{Token: os.Getenv("SYBILION_API_TOKEN")})
data, err := os.ReadFile("forecast_body.json")
if err != nil { log.Fatal(err) }
var body api.ForecastRequestV1
if err := json.Unmarshal(data, &body); err != nil { log.Fatal(err) }
acc, err := c.SubmitForecast(context.Background(), body)
if err != nil { log.Fatal(err) }
fmt.Println("job_id:", acc.GetJobId())
}library(jsonlite)
library(sybilion)
cl <- sybilion_client(token = Sys.getenv("SYBILION_API_TOKEN"))
payload <- jsonlite::fromJSON("forecast_body.json")
req <- ForecastRequestV1$new(
frequency = payload$frequency,
pipeline_version = payload$pipeline_version,
recency_factor = payload$recency_factor,
timeseries = payload$timeseries,
timeseries_metadata = TimeseriesMetadata$new(
title = payload$timeseries_metadata$title,
description = payload$timeseries_metadata$description,
keywords = payload$timeseries_metadata$keywords
),
backtest = payload$backtest,
soft_horizon = payload$soft_horizon
)
started <- cl$raw$ApiV1ForecastsPost(req)
cat("job_id:", started$job_id, "\n")import com.fasterxml.jackson.databind.ObjectMapper;
import com.sybilion.Client;
import com.sybilion.Options;
import com.sybilion.generated.model.ForecastRequestV1;
import java.nio.file.Files;
import java.nio.file.Path;
Client c = new Client(Options.builder().token(System.getenv("SYBILION_API_TOKEN")).build());
ObjectMapper om = new ObjectMapper().findAndRegisterModules();
ForecastRequestV1 req = om.readValue(
Files.readString(Path.of("forecast_body.json")), ForecastRequestV1.class);
var acc = c.defaultApi().apiV1ForecastsPost(req);
System.out.println("job_id: " + acc.getJobId());A successful submission returns 202 Accepted:
{
"job_id": "c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d",
"poll_url": "/api/v1/forecasts/c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d"
}Copy the job_id,it is needed to check the forecast job status and download artifacts. Validation errors return 422 with one {field, message} detail; see Errors & limits.
filters.limit
filters.limit controls how many drivers the pipeline considers. A higher limit gives the pipeline more candidates to evaluate, which improves forecast quality but also increases the time the job takes to complete.
Wait for job to complete
Forecasts typically take a few minutes. Poll GET /api/v1/forecasts/{id} until status is completed. All SDKs provide a helper that handles polling automatically.
JOB_ID="c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d"
until curl -sS -H "Authorization: Bearer $SYBILION_API_TOKEN" \
"https://api.sybilion.dev/api/v1/forecasts/$JOB_ID" \
| grep -q '"status":"completed"'; do
sleep 10
done
curl -sS -H "Authorization: Bearer $SYBILION_API_TOKEN" \
"https://api.sybilion.dev/api/v1/forecasts/$JOB_ID"job = client.wait_forecast(submit.job_id, poll_s=10.0, timeout_s=3600.0)
print("status:", job.status, "cost (cents):", job.eur_cents_final)
for a in job.artifacts or []:
print(" -", a.name, a.size, "bytes")ctx := context.Background()
job, err := c.WaitForecast(ctx, acc.GetJobId(), 10*time.Second)
if err != nil { log.Fatal(err) }
fmt.Println("status:", job.GetStatus(), "cost (cents):", job.GetEurCentsFinal())
for _, a := range job.GetArtifacts() {
fmt.Println(" -", a.GetName(), a.GetSize(), "bytes")
}job <- cl$wait_forecast(started$job_id, poll_s = 10, timeout_s = 3600)
cat("status:", job$status, "cost (cents):", job$eur_cents_final, "\n")
for (a in job$artifacts) cat(" -", a$name, a$size, "bytes\n")import java.time.Duration;
var job = c.forecasts().waitUntilSettled(
acc.getJobId().toString(), Duration.ofSeconds(10), Duration.ofHours(1));
System.out.println("status: " + job.getStatus() + " cost (cents): " + job.getEurCentsFinal());
for (var a : job.getArtifacts()) System.out.println(" - " + a.getName() + " " + a.getSize() + " bytes");When status is completed, the response lists the artifact files ready to download:
{
"job_id": "c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d",
"status": "completed",
"eur_cents_final": 5,
"artifacts": [
{
"name": "forecast.json",
"href": "/api/v1/forecasts/c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d/artifacts/forecast.json",
"content_type": "application/json",
"size": 4096
},
{
"name": "external_signals.json",
"href": "/api/v1/forecasts/c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d/artifacts/external_signals.json",
"content_type": "application/json",
"size": 2048
},
{
"name": "backtest_metrics.json",
"href": "/api/v1/forecasts/c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d/artifacts/backtest_metrics.json",
"content_type": "application/json",
"size": 1280
},
{
"name": "backtest_trajectories.json",
"href": "/api/v1/forecasts/c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d/artifacts/backtest_trajectories.json",
"content_type": "application/json",
"size": 8192
}
]
}If status is failed or canceled, the response includes a pipeline_error object with a code and a detail field explaining what went wrong.
Download artifacts
Use the name values from the artifacts array above. Artifacts are available at GET /api/v1/forecasts/{id}/artifacts/{name}.
| File | When present | Contents |
|---|---|---|
forecast.json | Always | Point forecasts and quantile bands for each horizon month. |
external_signals.json | Always | Ranked external drivers with importance, direction, and correlation scores. |
backtest_metrics.json | When backtest: true | Aggregated accuracy metrics (MAPE, RMSE) over rolling 6m / 12m / 24m / 60m windows. |
backtest_trajectories.json | When backtest: true | Per-fold actual vs forecast series for the last 12 months of history. |
JOB_ID="c7f2d8a9-3b4e-5f6a-7c8d-9e0f1a2b3c4d"
curl -sS -H "Authorization: Bearer $SYBILION_API_TOKEN" \
"https://api.sybilion.dev/api/v1/forecasts/$JOB_ID/artifacts/forecast.json"import json
data = client.get_forecast_artifact(submit.job_id, "forecast.json")
forecast = json.loads(data)
print(forecast["data"]["forecast_series"])import (
"io"
"net/http"
)
jobID := acc.GetJobId()
req, _ := http.NewRequestWithContext(ctx, "GET",
"https://api.sybilion.dev/api/v1/forecasts/"+jobID+"/artifacts/forecast.json",
nil,
)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SYBILION_API_TOKEN"))
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
buf, _ := io.ReadAll(resp.Body)
fmt.Println(string(buf))cl$raw$ApiV1ForecastsIdArtifactsNameGet(
job$job_id, "forecast.json", data_file = "forecast.json")
forecast <- jsonlite::fromJSON("forecast.json")
print(forecast$data$forecast_series)import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Path;
c.forecasts().downloadArtifactToFile(
job.getJobId().toString(), "forecast.json", Path.of("forecast.json"));
var tree = new ObjectMapper().readTree(Path.of("forecast.json").toFile());
System.out.println(tree.get("data").get("forecast_series"));Example forecast.json response (6-month horizon, one point shown):
{
"version": "1.1",
"data": {
"forecast_horizon": 6,
"forecast_start": "2026-01-01",
"forecast_end": "2026-06-01",
"forecast_series": {
"2026-01-01": {
"forecast": 78.40,
"quantile_forecast": { "0.1": 68.2, "0.5": 78.4, "0.9": 89.1 }
},
"2026-02-01": {
"forecast": 79.15,
"quantile_forecast": { "0.1": 68.8, "0.5": 79.2, "0.9": 89.9 }
}
}
}
}For the full schema of all artifact files, see Artifact download.
For error codes, validation details, and the full JSON envelope, see Forecast submission and Errors & limits.
Pricing
Billing applies only on 2xx responses. The cost includes a base fee plus a variable component that scales with the time the forecast job takes to complete.
A pre-charge hold is applied when the forecast job is successfully submitted. If there is not enough balance to satisfy the pre-charge hold, the operation is blocked.
See also
- API reference: POST /api/v1/forecasts · GET /api/v1/forecasts/:id · GET /api/v1/forecasts/:id/artifacts/:name.
- Find valid filter ids: Regions & categories.
- Clients: Using curl · Python SDK · Go SDK · R SDK · Java SDK.