Answers
A call returns one answer per question, under the question's id, beside a few facts about the call:
{
"status": "done",
"answers": {
"churn": {"type": "yesno", "state": "answered", "…": "…"},
"risk": {"type": "score", "state": "answered", "…": "…"}
},
"fits_run": 1,
"billable_decisions": 2,
"record": {"content_hash": "404d5ed6…"},
"core_hash": "4d24e9fc…",
"dataset_id": "sample:saas_churn"
}
| Field | |
|---|---|
status |
done, or pending while a first fit runs (below) |
answers |
one answer per question id |
fits_run |
fits this call ran; 0 when every question reused a fit |
billable_decisions |
answered cases this call counts toward billing (samples are counted but never charged) |
record.content_hash |
the record's content: the same bytes always give the same hash |
core_hash |
the engine build that answered. Same record + same question + same core_hash = same numbers. |
dataset_id |
the stored dataset or sample asked about (absent for inline data) |
shape |
with a shape: how the engine read the record |
Jev reports usage in tokens. Datagoat reports fits_run and billable_decisions, which are
what it charges for.
An answer
| Field | |
|---|---|
type |
the question's type |
state |
answered, refused or not_yet. Read it first. |
cases |
per case: p, reasons, levers when the engine found any, and level (score) or choice, most_likely, least_likely (choice) |
ranked, ranked_total |
rank only |
quality |
how the model did on rows it never saw (below); not on choice |
verdicts |
signed statements of the answer (below); answered questions only |
model_ref (model_refs for choice) |
the fitted model, for outcomes and drift |
polarity |
adverse or desirable: what outcome_is_desirable said or, when it was left out, how the engine read the outcome column's name; not on choice, where you always state it |
cache |
miss if this call fitted, hit if it reused a fit |
The answer state
Jev's Score and Choice answers carry a confidence, and your code decides what low confidence
means. Datagoat makes that call itself, before any number reaches you. Every answer has
a state:
state |
Means | Your code |
|---|---|---|
answered |
The record holds a pattern that held up on held-out rows. | Use the numbers. |
refused |
It doesn't. reasons lists why, as codes (for example no_finding_cleared or option_not_in_pattern); retry is unproductive. |
Don't retry: the same call returns the same refusal. Add columns, or ask about another outcome. |
not_yet |
Too few labeled rows to check a model. needs.labeled_rows says how many more. |
Add rows, then ask again. |
A refusal is an answer, not an error, and it is free. It is Datagoat's "I don't know", and it is as useful as a confident answer: it tells you the record can't support the claim.
In Jev, state is the input: the context a question is judged against. In Datagoat, the input is
the record, and state is the status of the answer.
An answered question can still decline single cases. A case with "refused": true has a
refused_reason, and it isn't billed. A case the model is confident is a no (a low p) is an
answer, and it is billed.
Quality
"quality": {"realised_lift": 4.48, "top_decile_lift": 4.44, "validation_scheme": "holdout", "confidence_tier": "high"}
| Field | |
|---|---|
top_decile_lift |
On rows the model never saw, the top tenth by p had the outcome this many times as often as average. |
realised_lift |
The lift of the pattern the fit found, over the record. |
validation_scheme |
how rows were held out: holdout (a random share) or out_of_time (the latest rows, when the record has time) |
confidence_tier |
the engine's grade of the evidence: high, moderate or low |
A question is answered only when both lifts are at least 1.5. Below that, it is refused.
Using an answer in code
Three paths, like Jev's confidence gating:
- Act.
stateisanswered, theconfidence_tieris one you trust (choiceanswers have no tier; theirstateis the gate), andp(or thelevel) clears the threshold for this action. - Review. The answer is in, but it is close to the threshold, the tier is
low, or the case is a person. Send it to someone with the reasons attached. - Don't act.
refusedornot_yet. Fall back to your default, and fix the record if the question matters.
Thresholds scale with the cost of being wrong. One answer can gate several actions at different thresholds: an email at 0.3, a call at 0.6, a discount at 0.8.
With band: true, each case also carries the engine's own gate: band (act, escalate or
refuse), band_reason and max_autonomy.
Reasons
Each answered case lists up to four reasons, strongest first:
{"feature_label": "support_tickets", "value": 12, "likelihood_direction": "higher", "strength": "moderate", "association_not_causal": true}
feature_labelis the column. With a shape, it is a feature the reading built, such asvisit_count_30d.valueis this case's value.likelihood_directionsays whether the column pushed this case's chancehigherorlowerthan the average case in the record.strengthisstrongormoderate.
Reasons come from the model's own terms. They are exact, and the signature covers them. They
describe association, not cause: association_not_causal is always true.
likelihood_direction is the direction of the chance, not whether that is good news. Whether it
is good news depends on outcome_is_desirable.
Verdicts
Every answered question carries a signed Verdict. A choice between outcomes (option_outcomes)
carries one per option. A refused or not_yet answer carries none. A Verdict records the model,
the record's content hash, the outcome and its polarity, the cases and their scores, and when it
expires: with its model, 90 days after the fit. A Verdict from a reused fit can expire sooner
than 90 days after the call.
Anyone can check a Verdict with dg_verify (POST /v1/verify), with no key needed, or against the
public keys at https://api.datagoat.io/.well-known/jwks.json. Pass the Verdict exactly as
received. The SDKs do it in one line: dg.verify_all(out). A Verdict that is not valid, or has
expired, is not evidence of anything. Ask the question again.
Pending
A first fit on a large record can take a minute or two. The call then returns
{"status": "pending", "task_id": "tk_…", "retry_after_ms": 2000}. Poll dg_poll with the
task_id until it's done. The SDKs wait for you. Re-sending the ask instead would start a second
fit. Send an idempotency_key and a retry returns the first call's answer. Answers are kept for
24 hours.
Determinism
The same record bytes, the same question and the same core_hash give the same numbers, every
time: p, levels, choices, order and reasons. A repeat call reuses the fit (cache: "hit",
fits_run: 0). One changed byte in the record is a new record, and it fits again.
Closing the loop
Datagoat can tell you whether acting on its answers worked. That takes three records: which cases you acted on, how far each one moved, and what finally happened. Jev answers can't carry this, because Jev never sees the outcome.
Levers
Some answered cases carry levers: changes that would move the case's chance, as the engine
found them in the record.
"levers": [{"feature_label": "contract", "direction": "change", "likelihood_direction": "lower",
"magnitude": "substantial", "lever_token": "hsct1.…", "association_not_causal": true}]
A lever names the feature and the direction, never the target value. The target stays inside the
opaque lever_token. Levers appear only where the engine can show the changed state is reachable
on held-out rows. Many answers, and many cases, have none. That is an honest answer, not a gap.
Recording an action
After you act on a case through a lever, record it with dg_attest. Send the lever's token and
the feature's new value:
dg.attest(model_ref, "acct_0020", lever["lever_token"], post_value="two_year", acted_at="2026-10-01T09:00:00Z", event_id="crm-act-77")
# {"compliant": true, "dose_fraction": 1.0, "evaluated_feature": "contract", "evaluated_direction": "change", …}
The engine checks the new value against the target it hid in the token. It returns compliant
and dose_fraction: how far toward the target the case moved, where 1 means all the way.
evaluated_feature names the feature the token checks. A lever with several changes carries one
token, minted for one of them. Only compliant actions count as acted on. The ledger stores a hash
of the token, never the token.
Reporting outcomes
When you learn what happened, report it:
dg.report_outcomes(model_ref, [{"entity_id": "acct_0020", "outcome": 0, "observed_at": "2026-11-01", "event_id": "crm-881"}])
Outcomes are evidence about the model's track record. They never change a model or an answer.
Did acting work?
dg_evidence compares the reported outcomes of cases acted on with those of cases not acted on.
For example:
{"live": {"n_acted": 64, "n_not_acted": 211, "rate_acted": 0.09, "rate_not_acted": 0.21,
"diff": -0.12, "ci_low": -0.2, "ci_high": -0.03, "small_n": true},
"progress": {"entities_with_outcome": 275, "n_acted": 64, "n_not_acted": 211, "attestations": 70}}
live stays null until each group has 30 cases with an outcome. progress shows how far
there is to go. small_n is true below 100 per group. The comparison is observational: you
chose which cases to act on, so a difference is evidence, not proof of cause.
Watching for drift
When the record changes, ask again about the new version with refit_of set to the old
model_ref. Then call dg_drift with the new model_ref. The pattern is unchanged, changed
or no_prior, and the recommendation is keep, refit or abandon.
Cost
| Price | |
|---|---|
| An answered case | $0.00002 ($20 per million) |
| A fit | 1,000 free each month, then $0.01 |
Refusals, not_yet, declined cases, verification, samples, storage, drift, outcome reports, attestations, evidence |
free |