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:

  1. Act. state is answered, the confidence_tier is one you trust (choice answers have no tier; their state is the gate), and p (or the level) clears the threshold for this action.
  2. 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.
  3. Don't act. refused or not_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_label is the column. With a shape, it is a feature the reading built, such as visit_count_30d.
  • value is this case's value.
  • likelihood_direction says whether the column pushed this case's chance higher or lower than the average case in the record.
  • strength is strong or moderate.

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