Spavik

Python SDK

Evaluate, train, predict and forecast with the synchronous and asynchronous Python SDK.

pip install spavik            # with pandas: pip install "spavik[pandas]"

The SDK requires Python 3.11 or later.

from spavik import Spavik

spavik = Spavik()                                   # reads SPAVIK_API_KEY
model  = spavik.train("churn.csv", target="churn")  # free
print(model.predict({"plan": "pro", "seats": 12, "tickets_90d": 3}))

Two things to know before the first call: your key, and the name of the column to predict.

The operations

The ones that carry a model identifier live on the Model object, so nobody has to retype a mdl_... by hand.

CallCost
spavik.evaluate(data, target=...)free
spavik.train(data, target=...)free
spavik.backtest(data, target=..., horizon=...)free
spavik.forecast(data, target=..., horizon=...)horizon × the engine's credits per call, all series included; 1 credit per step on the default engine
spavik.models()free
spavik.usage()free
model.predict(rows)the engine's credits per row, 1 on the default engine
model.submit_outcomes(outcomes)free
model.performance(days=30)free
model.refresh()free
model.archive(), spavik.archive(model_id)free
spavik.restore(model_id)free

models() is an iterator: pagination happens on its own. A model exposes its engine and features, the columns predict expects. The raw API fields are in model.data, for example model.data.get("target").

Engine selection

Training and evaluation accept engine_family; omitting it lets the API choose its configured default family. Read GET /v1/engine-families for available families. The model's family cannot change. Predictions accept engine_version to select an exact version or latest alias within that family. Forecast and backtest accept both options. Available versions and prices come from GET /v1/engines. Keep the exact engine from the prediction result with its model version.

Check before you train, or before you pay

evaluate() returns structural validation, a trial and the API's assessment. backtest() validates a series and replays its history against the required references, without returning forecast values. Both are free.

report = spavik.evaluate("churn.csv", target="churn")
print(report["assessment"]["status"])
print(report["assessment"]["metric"], report["assessment"]["message"])
print(report.get("hint"))

check = spavik.backtest(
    "sales.csv", target="sales", horizon=7, timestamp="date",
    point_estimate="median", quantiles=[0.1, 0.5, 0.9],
)
print(check["assessment"])
print(check.get("backtest"))

Read the evaluation guide before interpreting a score. The tabular trial uses a random holdout, with no temporal or group split controls. A better comparison on the sample does not establish future reliability.

Configuration

import os
from spavik import Spavik

spavik = Spavik(
    api_key=os.environ["SPAVIK_API_KEY"],
)

The key is sent in X-Spavik-Api-Key. Omitting api_key reads SPAVIK_API_KEY; an explicitly empty string disables the environment fallback. base_url and SPAVIK_BASE_URL are optional overrides. The SDK defaults to https://api.spavik.co. Set an override only when needed; base_url takes precedence.

Forecast options and results

forecast = spavik.forecast(
    "sales.csv", target="sales", horizon=7, timestamp="date",
    point_estimate="median", quantiles=[0.1, 0.5, 0.9],
)
print(forecast.points)  # parsed rows; no method call in Python
print(forecast.csv)
print(forecast.get("point_estimate"), forecast.predictions_remaining)

forecast.predictions_remaining copies the X-Spavik-Predictions-Remaining header, or is None when the API does not send it. forecast.cost is None: the API does not return the amount charged. See Costs and credits.

item_id names the series-ID column for multiple series. Match the backtest's history, horizon, column roles, point estimate and quantiles. The API default point estimate is mean. See series preparation for calendar constraints.

Observe performance

report = model.performance(days=30)
if report is not None:
    observed = report.get("observed")
    if observed is not None:
        print(observed["assessment"], observed["metrics"], observed["baselines"])
        print(observed["metric_context"], report.get("evaluation"))

Call this after reporting outcomes. observed: null means no client-reported predictions are available for comparison; API failures are raised. The SDK preserves API scores, baselines, undefined reasons and assessment without recalculating them.

Archive and restore

Plans cap the number of active models. archive() stops a model from serving and frees its slot; everything is kept, and spavik.restore(model_id) brings it back. Restore takes the identifier, not the model: an archived model cannot be read with model(), the API answers MODEL_ARCHIVED.

model.archive()
for m in spavik.models(status="archived"):
    print(m.id, m.name)
spavik.restore(model.id)

Permanent deletion is not in the library.

The data format is yours

spavik.train(df, target="churn")                  # pandas DataFrame
spavik.train("data/churn.csv", target="churn")    # file path
spavik.train([{"plan": "pro", "churn": 0}], ...)  # list of dicts

CSV, JSON, semicolons or commas, quoted or not: the library sorts it out, and retypes numbers along the way.

What is refused before being sent

The library has your data in hand, the API does not yet. Four refusals happen locally, with no network call and no spend.

spavik.train(eight_rows, target="churn")
# SpavikDataError: training needs at least 10 rows, 8 provided. Nothing was sent.

spavik.train(df, target="churn_90d")
# SpavikDataError: the target column 'churn_90d' is missing from the data.
#                  Columns found: plan, seats, tickets_90d, churn. Did you mean 'churn'?

spavik.forecast(three_points, target="sales", horizon=14)
# SpavikDataError: the series has 3 point(s) for a horizon of 14. A series shorter
#                  than its horizon yields nothing usable: provide at least 14 points,
#                  preferably several times more.

model.submit_outcomes([{"request_id": "...", "outcome": 1}])
# SpavikDataError: 1 outcome(s) out of 1 have no 'actual' field, the value actually
#                  observed. Fields found on the first: request_id, outcome.

The messages are the same, word for word, in the JavaScript library: a conformance suite replays one scenario across both, and across the MCP server.

Errors

One class per family, all subclasses of SpavikError, each carrying the machine code, the detail returned by the API, the status and the request_id.

from spavik import SpavikCreditsExhausted

try:
    model.predict(rows)
except SpavikCreditsExhausted as exc:
    print(exc.code, exc.detail, exc.request_id)
    print(spavik.usage()["predictions_remaining"])
ClassWhen
SpavikDataErrorlocal refusal, nothing was sent
SpavikAuthErrorkey missing, invalid, or insufficient
SpavikCreditsExhaustedbalance too low; read usage() for what is left
SpavikQuotaExceededa plan limit is reached: active models, rows
SpavikValidationErrorthe API refuses the request as formed
SpavikNotFoundthe resource does not exist in this workspace
SpavikConflictreplay, work in progress, nothing to integrate
SpavikRateLimitedtoo many calls
SpavikUnavailabledependency missing or asleep
SpavikServerErrorunexpected API failure
SpavikTransportErrornetwork, or non-JSON response

Normal states, not errors

model.refresh() returns None only when there are no pending outcomes. Other API failures are raised. model.performance() preserves the API report; observed: null means there are no measured outcomes to compare. An API failure is raised rather than converted into an empty performance report.

Async

Same names, same refusals, same results.

from spavik import AsyncSpavik

async with AsyncSpavik() as spavik:
    model = await spavik.train("churn.csv", target="churn")
    print(await model.predict(rows))

Retries

The SDK follows the API retry policy for network failures, eligible 429/5xx responses and REQUEST_IN_PROGRESS. It keeps the same idempotency key during an invocation's internal retries. Replays are protected while the API record is retained (24 hours by default for completed responses).

Keep exc.idempotency_key and exc.request_id after an uncertain failure. Calling the SDK operation again generates a new key and may execute or charge again. REQUEST_INTERRUPTED and IDEMPOTENCY_RESPONSE_UNAVAILABLE stop automatic retries; inspect the operation before continuing.

max_retries counts every attempt, the first included: the default is 3, and max_retries=1 disables retries. Without Retry-After, the default backoff is exponential with jitter, from 0.5 s up to 8 s.

Spavik(max_retries=1)                    # a single attempt, no retry
Spavik(backoff=lambda n: n * 1.0)        # linear backoff, n is the attempt number

Webhooks

verify checks the Spavik-Signature header and returns the decoded event. It raises SpavikSignatureError when the signature, the timestamp or the header is invalid. With Flask:

import os

from flask import Flask, request
from spavik import SpavikSignatureError, verify

app = Flask(__name__)

@app.post("/spavik/webhook")
def spavik_webhook():
    try:
        event = verify(request.get_data(), request.headers, os.environ["SPAVIK_WEBHOOK_SECRET"])
    except SpavikSignatureError:
        return "", 400
    # Store event["id"] and the event durably, then acknowledge.
    return "", 204

The body must be the bytes as received: a parsed and re-serialised body does not produce the same signature. See Webhooks for the delivery contract.

Debugging

SPAVIK_LOG=debug python my_script.py
spavik POST /v1/models  idem=a3f1b2c4  23.81s  201  req_atZusk...  key=sk-spv-api-bm7...Tr9x

The key is masked.

Default timeouts

OperationTimeout
train, evaluate, refresh120 s
forecast, backtest60 s
predict, submit_outcomes, performance, archive, restore30 s
models, model, usage, engines, engine_families15 s

The engine takes about ten seconds to answer when it wakes from idle, which is why the timeouts are generous.

How is this guide?

Last updated on

On this page