Skip to content

Python library

sh
pip install spavik            # with pandas: pip install "spavik[pandas]"
python
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.

CallCost
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.

python
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 period

weak 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.

python
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

python
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.

python
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.

python
from spavik import SpavikCreditsExhausted

try:
    model.predict(rows)
except SpavikCreditsExhausted as exc:
    print(exc.code, exc.balance, exc.request_id)
ClassWhen
SpavikDataErrorlocal refusal, nothing was sent
SpavikAuthErrorkey missing, invalid, or insufficient
SpavikCreditsExhaustedbalance too low, carries balance
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
SpavikTransportErrornetwork, 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.

python
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.

python
Spavik(max_retries=1)                    # no retries
Spavik(backoff=lambda n: n * 1.0)        # linear backoff

Webhooks

python
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

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

The key is masked.

Default timeouts

OperationTimeout
train, refresh120 s
forecast60 s
backtest60 s
validate120 s
predict, submit_outcomes, performance30 s
models, usage, predictions15 s

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

Part of this documentation is generated from the OpenAPI contract.