SovrGPT Docs
API

Decision Engine API (POST /v1/decisions)

Decision Engine API – send text or JSON plus questions with fixed options, get probabilities and a recommendation back in milliseconds.

The Decision Engine answers questions with a fixed set of options, such as: which team is responsible, how urgent is this, does a human need to review? It generates no text. For every question the model runs one forward pass, and we read the probability of each allowed option directly. That is why a decision comes back in milliseconds, where a chat answer takes seconds.

Base URL: https://sovrgpt.com/api/v1 · Scope: decisions · Auth: Authorization: Bearer sov_…

Changed on 23 Sep 2026: without a policy, the default mode is now threshold (previously review_only), and the double check is on. How to keep the old behaviour is described under Automatic or human. Limits, prices and model versions may still change.

Request a decision

curl https://sovrgpt.com/api/v1/decisions \
  -H "Authorization: Bearer $SOVR_KEY" -H "content-type: application/json" \
  -d '{
    "preset": "ticket-routing-v1",
    "state": "Since this morning I cannot log in. My invoice was also charged twice — please check this urgently."
  }'

Without a policy the default { "mode": "threshold", "threshold": 0.85, "stability_check": true } applies.

Response (abridged). The probabilities come from a measurement on sovr-decision-v2; recommendation, warning signals, double check, usage and timings were added for illustration:

{
  "object": "decision",
  "id": "dec_3f1c…",
  "request_id": null,
  "status": "completed",
  "model": { "id": "sovr-decision-v2", "runtime": "…", "operator": "…", "country": "IE", "prompt_version": "…", "via": "platform" },
  "preset": { "id": "ticket-routing-v1", "version": "1.0.0" },
  "answers": {
    "team": {
      "type": "choice",
      "value": "support",
      "probabilities": { "support": 0.6512, "billing": 0.0367, "sales": 0.0044, "unclear": 0.3076 },
      "max_probability": 0.6512,
      "margin": 0.75,
      "margin_is_lower_bound": false,
      "normalized_entropy": 0.58,
      "calibration": { "status": "uncalibrated" },
      "recommendation": "review",
      "reasons": ["below_threshold"],
      "stability": { "rotations": 2, "stable": true, "alternatives": [] },
      "abstain": true,
      "abstain_reasons": ["below_threshold", "calibration_unavailable"],
      "diagnostics": { "label_mass": 0.9967, "low_label_mass": false, "missing_labels": [] }
    },
    "urgency": { "type": "score", "value": 3, "expected_index": 3.29, "normalized_score": 0.82, "max_probability": 0.7042, "recommendation": "review", "reasons": ["below_threshold"], "stability": null, "…": "…" },
    "needs_human": { "type": "binary", "value": true, "probability_true": 0.9933, "recommendation": "act", "reasons": [], "stability": null, "…": "…" },
    "language": { "type": "choice", "value": "de", "max_probability": 0.9993, "recommendation": "act", "reasons": [], "stability": { "rotations": 2, "stable": true, "alternatives": [] }, "…": "…" }
  },
  "policy": { "mode": "threshold", "threshold": 0.85, "stability_check": true },
  "usage": { "input_tokens": 1290, "output_tokens": 6, "evaluated_questions": 4, "forward_passes": 6 },
  "timing_ms": { "total": 430, "inference_max": 405, "inference_sum": 1030 }
}

How to read it: team and urgency are below the 0.85 threshold and go to a human, needs_human and language are unremarkable. Two of the four questions are choice questions, hence six passes instead of four.

recommendation rates how reliable the answer is, not what it says. needs_human: true with "act" means the engine decided, without any warning signal, that a human should look at the ticket. Your application can route it there without a further check.

Request

FieldRequiredMeaning
state or messagesexactly oneThe state to judge: text, a JSON object/array or a conversation ([{role, content}], text only). Treated as data, never as instructions.
questions or presetexactly oneYour own questions (below) or a server-side preset (GET /v1/decisions/presets).
modelnosovr-decision-v1 or sovr-decision-v2. Omitted: the organisation's default.
imagesnoUp to 2 images as [{ "data_url": "data:image/png;base64,…" }], ≤ 4 MB each. data: URLs only; the service never fetches remote URLs.
policyno{ "mode": …, "threshold": …, "stability_check": … }, every field can be omitted on its own. Default: threshold, 0.85, double check on. See Automatic or human.
request_idnoFree-form, echoed back (correlation).

Question types

{
  "team":  { "type": "choice", "instructions": "Which team first?", "options": [ { "id": "support", "description": "technical" }, { "id": "billing" }, { "id": "unclear", "description": "cannot be assigned" } ] },
  "urgency": { "type": "score", "instructions": "How urgent?", "levels": ["not", "slightly", "normal", "urgent", "immediately"] },
  "needs_human": { "type": "binary", "instructions": "Does a human need to review?", "true_description": "yes", "false_description": "no" }
}
TypeResult
choice — 2–26 optionsvalue = option id with the highest probability, probabilities over all options
score — 2–10 ordered levelsvalue = most likely level (0-based), expected_index = Σ p·i, normalized_score = expected/(n−1). A score is not a physical scale.
binaryvalue = true/false, probability_true

Order matters: options and questions are evaluated in source order, and a model may react to the order. For choice questions the engine checks this with the double check. For scores the order is the meaning and stays as you send it.

Presets

A preset binds questions and options to an id; you send only preset and state. preset and questions are mutually exclusive (400). Audience, situation, example call and how to use the answer for all ten presets are on the Use cases page.

presetNameQuestions
ticket-routing-v1Ticket routingteam, urgency, needs_human, language
inbox-document-v1Inbox document classificationdoc_type, has_deadline, action_level, language
customer-feedback-v1Customer feedback analysissentiment, churn_risk, topic, needs_response
lead-qualification-v1Lead qualificationfit, timeline, budget_signal, decision_maker
behoerden-posteingang-v1Public-authority inboxart, zustaendigkeit, fristrelevant, eskalation
antrag-vollstaendigkeit-v1Application complete?vollstaendig, hauptmangel, widerspruch, naechster_schritt
veroeffentlichung-check-v1Publication release checkpersonenbezug_dritter, art9_daten, tonalitaet, freigabe
agent-handoff-v1Agent handoffaction, risk, sensitive_data
grounding-check-v1Grounding checksupport, unsupported_claims, contradiction, verdict
agent-step-risk-v1Agent step gateirreversible, effect, risk
  • GET /v1/decisions/presets returns the full questions per preset, plus audience and example_state (a realistic example state).
  • Three presets expect a JSON object as state: antrag-vollstaendigkeit-v1 (an application with pflichtfelder, erforderliche_unterlagen, eingereichte_unterlagen, unterschrift_vorhanden, anmerkung), grounding-check-v1 (question, sources, answer) and agent-step-risk-v1 (the agent's goal, current page and planned step, in the example ziel, seite, schritt). Their example_state is therefore JSON text; when calling, send the object directly, not as a string.
  • Every choice question of a preset has a domain abstention as its last option (unclear or other). If the model picks it, reasons contains chose_abstain_option and the recommendation is "review", except in autonomous mode.
  • The same engine with the same presets is available in chat as the “Entscheiden” (decide) tool (decision_engine) and in the MCP server as sovrgpt_decide / sovrgpt_list_decision_presets.

Models

ModelOperated inImagesCharacter
sovr-decision-v1 (default)Germany (STACKIT)yesvery fast, very decisive: probabilities almost always near 1.0 or 0
sovr-decision-v2EU, a European partner data centre (Ireland/Finland), zero retention by contractyesfiner-grained probabilities, suited to threshold logic

Both run on open base models; we decide which runtime computes. Which version applies without a model field and where it computes is listed per organisation under GET /v1/decisions/models. Owners and admins change the default under Decision Engine → Settings of this organisation.

Latency. Measured from Jena, warm, a ticket text with four options, without the double check: 65 ms per question on sovr-decision-v1, 198 ms on sovr-decision-v2; four questions in parallel 123 ms and 397 ms respectively. A chat answer to the same text on the same models takes 5–30 s. These values are not a guarantee; they depend on the provider, the load and the length of the state. The double check's second pass runs at the same time as the first, so we expect only a small increase; we have not re-measured it yet. The actual duration is part of every answer under timing_ms: inference_max is the longest single pass, inference_sum the sum of all.

Automatic or human: policy and recommendation

Every answer carries a recommendation: recommendation: "act" means your application can go ahead with the answer; "review" means a human should check first. reasons says why. The evaluation itself is complete either way. The engine never acts itself: binding approvals, access rights and actions stay with your application.

Three modes (policy.mode)

ModerecommendationFor
threshold (default)"act" as long as reasons is empty; otherwise "review"Automation with a recommendation: unremarkable answers run through, remarkable ones go to a human.
review_onlyalways "review"; reasons contains review_only_policyReview everything, for instance during the introduction phase or wherever human review is mandatory.
autonomousalways "act"There is no human, for example a character in a computer game or a machine that acts on its own. The warning signals are still listed in reasons. Your application can react more cautiously: fall back to a safe default, slow down, log.
policy fieldValuesDefault
modethreshold, review_only, autonomousthreshold
thresholda number greater than 0, up to and including 10.85
stability_checktrue / falsetrue

Every field can be omitted on its own; whatever is missing takes the default. threshold acts as the limit in threshold mode, only as a warning signal in autonomous (below_threshold in reasons), and not at all in review_only. The applied policy is echoed in every answer: "policy": { "mode": "threshold", "threshold": 0.85, "stability_check": true }.

Moving over from the behaviour before 23 Sep 2026: exactly the old behaviour at the old cost (every answer "review", one pass per question) is "policy": { "mode": "review_only", "stability_check": false }. With "policy": { "mode": "review_only" } alone the double check stays on. Every answer still goes to a human, who additionally sees unstable_under_reordering in reasons when a choice flips; in return every choice question costs twice the input tokens.

Warning signals (reasons)

reasons lists all warning signals of an answer, in every mode:

SignalMeaning
review_only_policyreview_only mode: every answer is presented to a human.
low_label_massLess than half of the probability sat on the allowed options (diagnostics.label_mass below 0.5). The model actually wanted to answer something else; the distribution is not reliable.
labels_missing_from_top_logprobsAt least one option was not among the model's candidates (diagnostics.missing_labels), and the best option is below 0.99. Missing options get probability 0; that is assumed, not measured.
below_thresholdmax_probability is below policy.threshold. Does not occur in review_only mode.
chose_abstain_optionA domain abstention was chosen, i.e. a choice option with the id unclear, other, unknown, none or unsure.
unstable_under_reorderingThe double check picked a different option with reordered options. It is the only signal that marks an otherwise unremarkable choice (enough label mass, no abstention) as uncertain even at max_probability 1.
stability_unavailableThe double check was requested, but its second pass failed (for example throttled or too late). The answer is complete but unconfirmed: stability is null, and in threshold mode the recommendation is "review".

An empty reasons means nothing remarkable was found. It is no guarantee that the answer is right; the values are uncalibrated (calibration.status: "uncalibrated").

Double check (stability_check)

With stability_check: true (default) the engine asks every choice question a second time, at the same time as the first pass. The options are shifted cyclically by half the list (rounded; with two options that means swapped). The answer in your order is the one that counts. The second pass does not feed into the result; it only shows whether the choice flips. If it picks a different option, stability.stable is false, stability.alternatives names the deviating choice, and reasons contains unstable_under_reordering.

Question typestability
choice with the double check{ "rotations": 2, "stable": true, "alternatives": [] } or stable: false with the deviating option
choice, second pass failednull, plus stability_unavailable in reasons
score, binarynull: they are not asked twice; for scores the order is the meaning
any question with stability_check: falsenull

When only the check pass fails (the runtime throttles, answers too late or unusably), the answer still arrives: evaluated in your order, with stability: null and the warning signal stability_unavailable. Because nothing is confirmed, threshold mode then recommends "review". If the first pass fails, i.e. the evaluated answer itself, the whole request fails (see Errors).

Why it exists. A choice answer can flip purely because of the order of the options. On our internal test set that was rare, 3 of 86 choice answers per version. On sovr-decision-v1, however, it happened precisely at a displayed certainty of around 100 %. Example: a complete application (antrag-vollstaendigkeit-v1) got naechster_schritt: "bearbeiten" (process) with 1.00 in source order, and "nachfordern" (request missing documents) with 0.97 with the options reordered. Neither the probability nor margin nor a threshold warns about this; only the second pass shows it. On sovr-decision-v2, answers only flipped where they were uncertain anyway.

This is what that case looks like in the answer (reconstructed, abridged):

"naechster_schritt": {
  "type": "choice",
  "value": "bearbeiten",
  "max_probability": 1,
  "recommendation": "review",
  "reasons": ["unstable_under_reordering"],
  "stability": { "rotations": 2, "stable": false, "alternatives": ["nachfordern"] },
  "abstain": true,
  "abstain_reasons": ["unstable_under_reordering", "calibration_unavailable"],
  "…": "…"
}

In autonomous mode recommendation would stay "act" and abstain false; reasons would still contain unstable_under_reordering.

Switching it off: "policy": { "stability_check": false }. You then get one pass per question, stability is null, and the unstable_under_reordering signal disappears. Only do this if you secure the order some other way. What the double check costs is under Cost.

Legacy fields abstain and abstain_reasons

They remain for existing integrations:

  • abstain is true exactly when recommendation is "review".
  • abstain_reasons equals reasons plus calibration_unavailable when abstain is true, and is empty otherwise.
  • In autonomous mode abstain is therefore always false and abstain_reasons empty; the warning signals are only in reasons.

New integrations use recommendation and reasons.

What the numbers mean

  • probabilities is the distribution over your options. max_probability: 0.92 means the model gives that option 92 % of the mass. It does not mean it is 92 % likely to be correct; calibration.status is always uncalibrated as of today. On our internal test set of 122 cases, both versions were over 95 % correct. That is a snapshot on our own examples, not a promise for your data.
  • diagnostics.label_mass tells you how much probability sat on the allowed options at all. Below 0.5 the model wanted to write something else. The distribution is then an artefact, diagnostics.low_label_mass is true, and reasons contains low_label_mass.
  • sovr-decision-v1 mostly answers 1.0 / 0 / 0, with few shades in between. A percentage threshold rarely bites there. Two other nets are more reliable: the domain abstention (unclear, other), which triggers chose_abstain_option regardless of the percentage, and the double check.

margin — the number a threshold belongs on

margin is the gap between the best and the second-best option, measured as log p(best) − log p(second best) in nats. Unlike max_probability it has no ceiling: two answers that both come out as max_probability: 1 may have a gap of 8.5 or 19.2.

On our internal test set (463 answers per version), max_probability separates correct from incorrect answers on sovr-decision-v1 with an AUROC of 0.777. No threshold on it catches every error, because four of the eleven wrong answers carry max_probability of exactly 1. Using margin the AUROC is 0.867, and at a gap of 20 or more half of all answers ran through without a single error in that measurement.

More decimal places on max_probability would not fix this. One reason is rounding to four places. The other: for roughly a quarter of answers only one option appears among the model's candidates at all. The probability is then exactly 1 by construction, and there is no resolution left to recover. For that case margin_is_lower_bound: true is reported: the gap was computed against the candidate cut-off and is a lower bound. The true gap is at least that large, never smaller.

A workable threshold is not a guarantee. Across 400 random halvings of our test set, no error slipped through in 72 % of them, but in the worst case ten did. margin is a second net; the first remains the domain abstention option. Which gap holds for your data must be determined on your data. Our 20 applies to our test set.

recommendation (and with it the legacy field abstain) is derived from max_probability, threshold and the other warning signals. margin is returned but not yet part of the recommendation. If you want to gate on margin, do it in your application.

Errors

StatuscodeMeaning
400invalid_request_error, invalid_json, images_unsupportedinvalid body, including an unknown field in policy
401—key missing, invalid or revoked
402org_spend_limit, user_spend_limitspending limit reached
403missing_scopekey lacks the decisions scope
404unknown_preset, model_not_found
413request_too_large
429upstream_rate_limitedthe runtime is throttling right now (type: "rate_limit_error"). Wait briefly and retry; nothing is billed. Until 23 Sep 2026 this came back as 502.
502 / 504upstream_error, upstream_timeoutthe evaluated answer (first pass) did not arrive usably or in time. There is no partial answer, and nothing is billed. If only the second pass of the double check fails, that is not an error: the answer arrives with stability: null and stability_unavailable.
503runtime_unavailableno measured runtime is reachable in this deployment. A different model is never used silently.

Limits and cost

Limits

1–16 questions · state ≤ 16,000 characters · question ≤ 1,000 characters · description ≤ 512 characters · body ≤ 64 KiB. Too large → 413. Unknown fields are rejected with 400, inside policy as well, so that a tenant_id or tools in the body never looks as if it had an effect.

Cost

  • Billing counts input tokens plus one output token per forward pass, only for passes the runtime answered usably.
  • With the double check, every choice question uses twice the input tokens (two passes over the same state) and two output tokens. Score and yes/no questions cost the same as without the double check.
  • usage.forward_passes counts all answered passes, usage.input_tokens and usage.output_tokens include both. usage.evaluated_questions counts the questions. Example ticket-routing-v1: four questions, two of them choice questions → six passes.
  • A failed check pass costs nothing and is missing from forward_passes. If the request fails as a whole (error code), nothing is billed.

Privacy

State, questions and answers are not stored; your usage records only the billed token counts. Images are never fetched: only data: URLs are accepted. The processing location is part of every response (model.operator, model.country).

Further endpoints

  • GET /v1/decisions/presets: the ten server-side presets with audience (audience), example state (example_state) and questions.
  • GET /v1/decisions/models: this organisation's versions, default, operator, country, measurements, limits.

A playground with real inference is available after signing in, under Decision Engine in the sidebar.

Decision Engine API (POST /v1/decisions)