Docs
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.
On this page
Prefer a machine-readable reference? openapi.yaml — every field checked against the running service, not written from memory.
Base URL https://api.kirelta.com. 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
curl
curl -X POST "https://api.kirelta.com/fit?model=credit-risk" \
-H "X-API-Key: kir_your_key" \
-H "Content-Type: application/json" \
-d '{"rows": [[0.8, 1.2, 0.4], [1.1, 0.9, 0.6]]}'Python
import requests
requests.post(
"https://api.kirelta.com/fit",
params={"model": "credit-risk"},
headers={"X-API-Key": "kir_your_key"},
json={"rows": healthy_rows}, # [[0.8, 1.2, 0.4], [1.1, 0.9, 0.6], ...300 healthy rows]
).json()JavaScript
await fetch("https://api.kirelta.com/fit?model=credit-risk", {
method: "POST",
headers: {
"X-API-Key": "kir_your_key",
"Content-Type": "application/json"
},
body: JSON.stringify({ rows: healthyRows })
}).then(r => r.json());Returns
{
"status": "fitted",
"model": "credit-risk",
"n": 300,
"features": 3,
"baseline_fit": "clear"
}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
curl
curl -X POST "https://api.kirelta.com/assess?model=credit-risk" \
-H "X-API-Key: kir_your_key" \
-H "Content-Type: application/json" \
-d '{"rows": [[0.9, 1.0, 0.5], [1.3, 0.8, 0.7]]}'Python
r = requests.post(
"https://api.kirelta.com/assess",
params={"model": "credit-risk"},
headers={"X-API-Key": "kir_your_key"},
json={"rows": recent_rows}, # [[0.9, 1.0, 0.5], [1.3, 0.8, 0.7], ...recent rows]
)
verdict = r.json()["verdict"] # TRUSTED | DEGRADED | UNTRUSTED | INSUFFICIENT_EVIDENCEJavaScript
const r = await fetch("https://api.kirelta.com/assess?model=credit-risk", {
method: "POST",
headers: {
"X-API-Key": "kir_your_key",
"Content-Type": "application/json"
},
body: JSON.stringify({ rows: recentRows })
});
const { verdict, action } = await r.json();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,
"shifted": true,
"n": 80
},
"notes": ["The data this model was trained on may itself contain anomalies..."],
"note_codes": ["baseline_may_contain_anomalies"],
"input_collapsed": false,
"typicality_median": 0.4913,
"temporal": { "elevated": false, "note": "..." }
}| verdict | TRUSTED · DEGRADED · UNTRUSTED — the call itself. Branch on this. |
| action | The engine’s recommendation in words, e.g. ALLOW, REVIEW (elevated abstention), BLOCK / escalate to a human. |
| flagged_rate | Fraction of rows in this batch that fell outside the trained range. 0.0–1.0. |
| drift_alarmed | true when the drift alarm fires. Its false-alarm rate is bounded over the whole run, not per batch. |
| n | How many rows were assessed. |
| top_features | Column 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_stability | The 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. 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 value is this batch’s evidence against "no drift"; the alarm fires the moment it reaches alarm_threshold. 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. Each call is judged on its own rows, so read it per batch. |
| input_collapsed typicality_median | The failure the drift alarm cannot see. typicality_median is how typical your live points looked, 0.0–1.0; around 0.50 is healthy. When it climbs towards 1.00, nearly every row looks perfectly normal — which real data never does — and input_collapsed goes true. Two causes: a stuck feed (a frozen sensor, a job returning defaults or column means, a join matching one row), or a shape change that kept the same mean and covariance. Nothing looks anomalous in either case, so the verdict stays TRUSTED — this field is the only thing that tells you. |
| drift_started_at | Index of the row where drift was declared. Absent when nothing alarmed. It 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_importance | The real share of the deviation each feature carries, summing to 1.0 — not just an ordering. top_features is kept for compatibility. |
| notes note_codes | Diagnostics about this model, in plain sentences. notes is what to show a person; note_codes is what to branch on — a stable identifier per diagnostic, so your handler does not depend on wording that may be rephrased. The one worth wiring up first is baseline_may_contain_anomalies: the data you trained on may itself contain anomalies, which changes how every verdict from that baseline should be read. Treat the list as open — new codes are added over time, so ignore ones you do not recognise rather than failing on them. |
| slow_drift_rate | Present only when the baseline itself is aging. Catches the slow drift that never trips a single batch. |
| temporal | Whether the drift alarm is fully reliable for this batch. The alarm assumes the rows you send are independent of one another. When they are not — readings from one session, or a sorted export — the alarm becomes less reliable for that batch and temporal.elevated reports true, with temporal.note explaining it in words. The per-row flags and the flagged rate are unaffected. Branch on elevated. Kirelta tells you rather than letting you over-trust the alarm. |
| trend | Present 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.
Python
import requests
KIRELTA = "https://api.kirelta.com"
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"] in ("DEGRADED", "INSUFFICIENT_EVIDENCE"):
# degraded, or no conclusion either way: act, but flag it
log_for_review(verdict)
serve(predictions)
else:
serve(predictions) # TRUSTEDSame shape in JavaScript:
JavaScript
const KIRELTA = "https://api.kirelta.com";
const KEY = "kir_your_key_here";
async function canITrust(model, rows) {
const r = await fetch(`${KIRELTA}/assess?model=${model}`, {
method: "POST",
headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
body: JSON.stringify({ rows })
});
if (!r.ok) throw new Error(`assess failed: ${r.status}`);
return r.json();
}
const verdict = await canITrust("credit-risk", recentFeatureRows);
if (verdict.verdict === "UNTRUSTED") {
escalateToHuman(verdict); // don't act on the model's output
} else if (["DEGRADED", "INSUFFICIENT_EVIDENCE"].includes(verdict.verdict)) {
// degraded, or no conclusion either way: act, but flag it
logForReview(verdict);
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, predictions used this month, and 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 /alerts tells you whether the channel is working, not just what is configured. Alongside the URL and the format it returns delivery — how many of your recent alerts were actually accepted by your endpoint, when the last attempt was, and why it failed if it did. If consecutive_failures is non-zero the response also carries a plain-English delivery_warning. Poll it, or watch it in the console: a webhook that has silently stopped accepting deliveries is a monitoring tool you no longer have.
GET /health
No auth. Returns {"status":"ok"} when the engine is up.
When something fails
| 400 | The body wasn’t what the endpoint expected. The message says what was wrong. An error about your data also carries a stable code, such as baseline_too_small or feature_mismatch, and the numbers the message states, such as min_rows: branch on the code, not on the wording. |
| 401 | Missing or revoked API key. |
| 404 | That model or record doesn’t exist in your workspace — for example, reading the details of a model that was never fitted. |
| 409 | That model hasn’t been fitted yet: /assess answers 409 until /fit has run for it. |
| 429 | One of two limits, told apart by the body. The monthly prediction limit: detail reads “You have reached your monthly prediction limit. Upgrade your plan to continue processing predictions.” and nothing more is processed until the month resets or you upgrade. Or a rate limit: back off and retry. |
A model exists only after /fit. If /assess answers 409, fit the baseline first.