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.
| Call | Cost |
|---|---|
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 dictsCSV, 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"])| Class | When |
|---|---|
SpavikDataError | local refusal, nothing was sent |
SpavikAuthError | key missing, invalid, or insufficient |
SpavikCreditsExhausted | balance too low; read usage() for what is left |
SpavikQuotaExceeded | a plan limit is reached: active models, rows |
SpavikValidationError | the API refuses the request as formed |
SpavikNotFound | the resource does not exist in this workspace |
SpavikConflict | replay, work in progress, nothing to integrate |
SpavikRateLimited | too many calls |
SpavikUnavailable | dependency missing or asleep |
SpavikServerError | unexpected API failure |
SpavikTransportError | network, 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 numberWebhooks
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 "", 204The 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...Tr9xThe key is masked.
Default timeouts
| Operation | Timeout |
|---|---|
train, evaluate, refresh | 120 s |
forecast, backtest | 60 s |
predict, submit_outcomes, performance, archive, restore | 30 s |
models, model, usage, engines, engine_families | 15 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