Skip to content

Forecasts

Async forecasts run on the Sybilion pipeline. You submit a ForecastRequestV1 and receive a job_id; you then poll until the job is settled; then you download the resulting JSON artifacts (point and quantile forecasts, driver attributions, optional backtest metrics and trajectories).

This page walks the full happy path with curl, the Python SDK, and the Go SDK. For the complete validation rules and per-field response shape see POST /api/v1/forecasts, GET /api/v1/forecasts/:id, and Artifact download.

When to use forecasts

  • You have a monthly time series with at least 40 observations (more for longer horizons — see below).
  • You want point forecasts and optionally quantile bands for the next 1–12 months.
  • You want explanation: per-driver importance and direction.
  • Optionally: rolling-window backtest metrics and trajectories.

For synchronous "what drivers correlate with my series?" answers without running the full pipeline, use Drivers instead.

1. Submit the job

A minimal valid body: pick a horizon and frequency, attach metadata and a timeseries map of YYYY-MM-DD → finite numbers. The example below uses 60 monthly points so it covers horizons up to 6.

bash
cat > forecast_body.json <<'EOF'
{
  "pipeline_version": "v1",
  "horizon": 6,
  "frequency": "monthly",
  "backtest": true,
  "recency_factor": 0.5,
  "strictly_positive": true,
  "timeseries_metadata": {
    "title": "Aluminum price in Europe USD/KG",
    "description": "Spot price of primary aluminum in the European market.",
    "keywords": ["aluminum", "metals", "commodities"]
  },
  "filters": { "categories": [101], "regions": [42] },
  "timeseries": {
    "2021-01-01": 1.95, "2021-02-01": 2.05
    /* … 58 more monthly points … */
  }
}
EOF

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.json | jq
python
import datetime as dt
import os

from sybilion import Client

client = Client(token=os.environ["SYBILION_API_TOKEN"])

# Build 60 synthetic monthly points (replace with your real series).
anchor = dt.date(2021, 1, 1)
timeseries = {
    anchor.replace(
        year=anchor.year + (anchor.month - 1 + i) // 12,
        month=(anchor.month - 1 + i) % 12 + 1,
    ).isoformat(): 100.0 + i * 0.5
    for i in range(60)
}

body = {
    "pipeline_version": "v1",
    "horizon": 6,
    "frequency": "monthly",
    "backtest": True,
    "recency_factor": 0.5,
    "strictly_positive": True,
    "timeseries_metadata": {
        "title": "Aluminum price in Europe USD/KG",
    },
    "timeseries": timeseries,
}

submit = client.raw.api_v1_forecasts_post(forecast_request_v1=body)
print("job_id:", submit.job_id)
go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"go.sybilion.dev/sybilion"
	api "go.sybilion.dev/sybilion/api"
)

func main() {
	c := sybilion.New(sybilion.Options{
		Token: os.Getenv("SYBILION_API_TOKEN"),
	})

	meta := api.NewTimeseriesMetadata("Aluminum price in Europe USD/KG")

	ts := map[string]float32{}
	anchor := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
	for i := range 60 {
		d := anchor.AddDate(0, i, 0).Format("2006-01-02")
		ts[d] = 100 + float32(i)*0.5
	}

	body := api.NewForecastRequestV1("v1", 6, "monthly", 0.5, *meta, ts)
	body.SetBacktest(true)
	body.SetStrictlyPositive(true)

	acc, _, err := c.DefaultAPI().
		ApiV1ForecastsPost(context.Background()).
		ForecastRequestV1(*body).
		Execute()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("job_id:", acc.GetJobId())
}

A successful submit returns 202 Accepted with a job_id (UUID) and poll_url. Validation errors are 422 with one {field, message} per response (fail-fast); see Errors & limits for the envelope.

Minimum series length

Horizon 13 needs 40+ monthly points; 46 needs 60+; 712 needs 120+. The latest observation must fall within the past 12 months. Full rules: POST /api/v1/forecasts.

2. Poll until the job settles

Forecasts run for tens of seconds to a few minutes. Poll GET /api/v1/forecasts/:id until settled is true, then read the resulting status (completed, failed, or canceled) and the artifacts array.

bash
JOB_ID="<paste job_id>"
API="https://api.sybilion.dev"
until curl -sS -H "Authorization: Bearer $SYBILION_API_TOKEN" \
  "$API/api/v1/forecasts/$JOB_ID" | jq -e '.settled == true' >/dev/null; do
  sleep 2
done
curl -sS -H "Authorization: Bearer $SYBILION_API_TOKEN" \
  "$API/api/v1/forecasts/$JOB_ID" | jq '{status, eur_cents_final, artifacts: .artifacts | map(.name)}'
python
job = client.wait_forecast(submit.job_id, poll_s=2.0, timeout_s=600.0)
print("status:", job.status, "cost (cents):", job.eur_cents_final)
for a in job.artifacts or []:
    print(" -", a.name, a.size, "bytes")
go
ctx := context.Background()
job, err := c.Forecasts().Wait(ctx, acc.GetJobId(), 2*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")
}

status == "completed" means a successful run with downloadable artifacts. failed and canceled ship a terminal_reason and (when available) a bounded pipeline_error JSON object — read those to surface the problem to the caller. The job ID stays valid afterward for replays of the read endpoints.

3. Download artifacts

Artifacts are streamed through the API at GET /api/v1/forecasts/:id/artifacts/:name — there are no direct storage URLs. Use the name values from the artifacts array.

bash
ART="forecast.json"
curl -sS -H "Authorization: Bearer $SYBILION_API_TOKEN" \
  -o "$ART" \
  "https://api.sybilion.dev/api/v1/forecasts/$JOB_ID/artifacts/$ART"
jq '.data.forecast_series | to_entries | .[0]' "$ART"
python
import json

from sybilion import ApiException

raw_api = client.raw  # underlying generated DefaultApi
try:
    body = raw_api.api_v1_forecasts_id_artifacts_name_get(
        id=submit.job_id, name="forecast.json"
    )
    forecast = json.loads(body)
    first_date, point = next(iter(forecast["data"]["forecast_series"].items()))
    print(first_date, "->", point.get("forecast"), point.get("quantile_forecast"))
except ApiException as exc:
    print("download failed:", exc.status, exc.body)
go
import (
	"encoding/json"
	"io"
	"net/http"
)

req, _ := http.NewRequestWithContext(ctx, "GET",
	"https://api.sybilion.dev/api/v1/forecasts/"+job.GetJobId()+"/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)
var envelope struct {
	Version string                 `json:"version"`
	Data    map[string]interface{} `json:"data"`
}
_ = json.Unmarshal(buf, &envelope)
fmt.Println(envelope.Version, "keys:", len(envelope.Data))

Artifact set

A successful run produces up to four files, all sharing the envelope { "version": "...", "data": {...} }:

FileWhenContents
forecast.jsonAlwaysPoint forecast and (when applicable) quantile bands per horizon.
external_signals.jsonAlwaysDriver attributions: importance, direction, correlation per driver.
backtest_trajectories.jsonWhen backtest: truePer-fold actual vs forecast (last 12 months retained).
backtest_metrics.jsonWhen backtest: trueAggregated metrics over rolling 6m / 12m / 24m / 60m windows.

Full schema for each file is in Artifact download.

Common errors

CodeCauseWhat to do
402Available balance below the hold for this job.Top up in the Developers Portal; recheck available_eur_cents on /me.
422Validation failure (one detail).Inspect details[0].field and details[0].message and fix the body.
429Per-minute submit cap or concurrent-job cap exceeded.Back off; check your tier on /tiers.
413Body over 2 MiB.Trim metadata or shorten the time series.

For the full catalog of error codes and the JSON envelope, see Errors & limits.

See also

[email protected] · Slack · Discord (links in Community page & header icons)