Python library
pip install spavik # with pandas: pip install "spavik[pandas]"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}))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.validate(data, target=...) | free |
spavik.train(data, target=...) | free |
spavik.backtest(data, target=..., horizon=...) | free |
spavik.forecast(data, target=..., horizon=...) | horizon x series |
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.predictions(limit=50) | free |
model.archive(), spavik.archive(model_id) | free |
spavik.restore(model_id) | free |
models() is an iterator: pagination happens on its own. A model knows its engine, its target and its features, the columns predict expects.
Check before you train, or before you pay
validate() returns what training would say of a table, plus a trial run of the engine that persists nothing: a verdict and the first thing to fix. backtest() does the same for a series, replaying its history against simply repeating the last period, without returning any forecast. Both are free.
report = spavik.validate("churn.csv", target="churn")
report["trial"]["verdict"] # {'level': 'usable', 'message': 'Accuracy of 0.83, against 0.50 for the majority class.'}
report["hint"] # the first thing to fix, or None
check = spavik.backtest("sales.csv", target="sales", horizon=7)
check["backtest"]["verdict"] # {'level': 'no_signal', 'message': '...'}
check["backtest"]["by_window"] # one score per replayed periodweak on a validation means the score is suspiciously high: a column is probably filled in after the event you predict. Remove it rather than train.
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, 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.balance, exc.request_id)| Class | When |
|---|---|
SpavikDataError | local refusal, nothing was sent |
SpavikAuthError | key missing, invalid, or insufficient |
SpavikCreditsExhausted | balance too low, carries balance |
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 |
SpavikTransportError | network, or non-JSON response |
Normal states, not errors
model.refresh() returns None when there is nothing to integrate, and model.performance() returns None when the analytics log is not in place. Neither forces you to catch an exception.
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
On 429, 5xx and network failures only, never on a 4xx. The idempotency key is replayed unchanged: a retry never bills twice.
Spavik(max_retries=1) # no retries
Spavik(backoff=lambda n: n * 1.0) # linear backoffWebhooks
from spavik.webhooks import verifier
event = verifier(request.body, request.headers, secret)The body must be the bytes as received: a parsed and re-serialised body does not produce the same signature.
Debugging
SPAVIK_LOG=debug python my_script.py
spavik POST /v1/models idem=a3f1b2c4 23.81s 201 req_atZusk... cle=sk-spv-api-bm7...Tr9xThe key is masked.
Default timeouts
| Operation | Timeout |
|---|---|
train, refresh | 120 s |
forecast | 60 s |
backtest | 60 s |
validate | 120 s |
predict, submit_outcomes, performance | 30 s |
models, usage, predictions | 15 s |
The engine takes about ten seconds to answer when it wakes from idle, which is why the timeouts are generous.