API

Three calls, and you have a verdict

Kirelta takes rows of numbers — the same feature vectors you feed your model — and tells you whether they still look like the data the model was trained on. Everything below is copy-paste ready.

Base URL . Authenticate data calls with the header X-API-Key. Get a key on the account page.

Train a baseline

Send a batch of rows that represent healthy behaviour — data from a period when the model was working well. Kirelta learns the shape of that data and picks its own monitor for it. Every row must have the same number of features.

Send outputs too. Add the predictions your model made on those same rows and Kirelta learns its normal behaviour as well — so it can later catch a model that has collapsed while the data going into it still looks completely ordinary. That failure is invisible to any tool watching inputs alone.
POST/fit?model=NAME

Returns

{ "status": "fitted", "model": "credit-risk", "n": 300, "features": 3, "variant": "static", "reason": "baseline looks unimodal (BIC favours K=1) -> calibrated single-Gaussian Kirelta", "variant_confidence": "high" }

Assess a batch

Now send recent rows — the ones you actually want a verdict on. Kirelta compares them against the baseline and answers in one object.

POST/assess?model=NAME

Read the verdict

This is the whole product. Everything the console shows you is built from these fields — nothing else is computed anywhere.

{ "model": "t_e573d9984466::credit-risk", "verdict": "UNTRUSTED", "action": "BLOCK / escalate to a human", "flagged_rate": 1.0, "drift_alarmed": true, "n": 80, "martingale": 2203.05, "alarm_threshold": 20.0, "drift_started_at": 1, "feature_importance": [ { "feature": 2, "importance": 0.3770 }, { "feature": 1, "importance": 0.3561 }, { "feature": 0, "importance": 0.2669 } ], "top_features": [2, 1, 0], "output_stability": { "active": true, "drift": true, "js_divergence": 0.117415, "pvalue": 0.0005, "n": 80 }, "notes": ["baseline looks contaminated: ~20% of 'normal' points are outliers..."], "variant": "static", "temporal": { "ac1": 0.024, "thinning_k": 1 } }
verdictTRUSTED · DEGRADED · UNTRUSTED — the call itself. Branch on this.
actionThe engine's recommendation in words, e.g. ALLOW, REVIEW (elevated abstention), BLOCK / escalate to a human.
flagged_rateFraction of rows in this batch that fell outside the trained range. 0.01.0.
drift_alarmedtrue when the sequential test fires. This is the anytime-valid alarm — its false-alarm rate is bounded over the whole run, not per batch.
nHow many rows were assessed.
top_featuresColumn indices that contributed most to the deviation, most unusual first.
feature_names
(send on /fit)
Optional, and worth more than it costs. Pass your column names as a list, in the same order as your rows, and every piece of evidence comes back named: {"feature": 2, "name": "cpu_load", "importance": 0.529}. Without it the engine can only say #2 — a fact, not an explanation. A list of the wrong length is rejected rather than applied, because a mislabelled feature is worse than an unlabelled one. The Python SDK reads them from a pandas DataFrame automatically.
persisted
(on /fit)
Whether this model reached disk. true means it survives a restart; false means it is live in memory only and a persistence_note explains why (no volume configured, or the write failed). A model that says fitted but persisted:false is one redeploy from gone. The /health endpoint reports the same fact service-wide as durable.
output_stabilityThe second layer — the model's own behaviour. Send outputs (the predictions your model made) to /fit and /assess and Kirelta also tests whether the model still behaves like itself, via a permutation test on the Jensen–Shannon divergence. Always present, including when it is switched off — a dark layer reports active: false and says why, because "no drift" from a layer that never ran means nothing.

The combination that matters: verdict: TRUSTED with output_stability.drift: true is the silent failure — nothing upstream changed and the model quietly stopped behaving like itself. Tools that watch only inputs miss it every time.
martingale
alarm_threshold
How close this batch came to the alarm. The martingale is accumulated evidence against "no drift"; the alarm fires the moment it reaches alarm_threshold (= 1/α). A batch at 9 / 20 is not alarmed and is not fine — it is nearly halfway to a block. Every other tool gives you only the binary. Evidence accumulates over the rows of a batch and resets on the next call, so read it per batch.
drift_started_atIndex of the row where drift was declared. Absent when nothing alarmed. Evidence has to accumulate before the alarm can fire, so this lands a few rows after the true onset (measured: ~4 rows past a hard mid-batch shift) — read it as "no later than here", not as the exact first bad row.
feature_importanceThe real share of the deviation each feature carries, summing to 1.0 — not just an ordering. top_features is kept for compatibility.
notesThe engine's own warnings. The one worth reading: "baseline looks contaminated" means the data you trained on may itself contain anomalies — which changes how you should read every verdict that comes from it.
slow_drift_ratePresent only when the baseline itself is aging. Catches the slow drift that never trips a single batch.
temporal.ac1Lag-1 autocorrelation measured in this batch. The drift alarm assumes your rows are exchangeable; when ac1 climbs above 0.3 that assumption weakens and the alarm becomes less reliable — the per-row flags stay valid. Kirelta measures this and tells you, rather than letting you over-trust the alarm.
trendPresent only when applicable: {"state":"recovering","window":8} means this model was alarmed recently and is now improving. It never replaces verdict — read both.

Wire it into your service

The usual shape: assess the batch before you act on the predictions, and branch on the verdict.

import requests KIRELTA = "" KEY = "kir_your_key_here" def can_i_trust(model, rows): r = requests.post(f"{KIRELTA}/assess?model={model}", headers={"X-API-Key": KEY}, json={"rows": rows}, timeout=10) r.raise_for_status() return r.json() verdict = can_i_trust("credit-risk", recent_feature_rows) if verdict["verdict"] == "UNTRUSTED": escalate_to_human(verdict) # don't act on the model's output elif verdict["verdict"] == "DEGRADED": log_for_review(verdict) # act, but flag it serve(predictions) else: serve(predictions) # trusted

Everything else

The rest of the surface, in one place.

GET/models

Every model in your tenant, and how many features each expects.

GET/model/meta?model=NAME

Which monitor Kirelta chose for this model and why — useful when you want to justify a verdict.

GET/usage

Your plan, checks used today, models against your limit.

POST/keys   POST/keys/revoke

Create and revoke API keys. Session auth (Authorization: Bearer), not key auth — or just use the account page.

GET/account/export   DELETE/account

Download everything held about your account, or erase it completely.

GET/events   GET/alerts   POST/alerts

Trust events — what changed. Kirelta POSTs an event to your webhook when a model's verdict changes, not on every reading: a model assessed a thousand times while broken sends one event, not a thousand. An escalation from degraded to untrusted always gets through, cooldown or not — hearing "degraded" and then silence is the most dangerous thing this system could do to you. Kirelta notifies; it never acts on your systems.

It only sees what you send it. There is no scheduler. If your service stops calling /assess, Kirelta goes quiet, and no event will tell you that you have gone blind — keep that check in your own uptime monitoring.

GET/health

No auth. Returns {"status":"ok"} when the engine is up.

When something fails

400The body wasn't what the endpoint expected. The message says what was wrong.
401Missing or revoked API key.
404That model hasn't been fitted in your tenant yet.
429Rate limited. Back off and retry.

A model exists only after /fit. If you get a 404 on /assess, fit the baseline first.