Calculations
This guide is for integrators who want to read the numbers Underwriting Guru computes, and — first — to understand where those numbers come from before trusting any of them.
All endpoints referenced here live under /v1 (e.g.
https://api.example.com/v1/calculation-packs). Requests are authenticated the same way as the rest
of the API: Authorization: Bearer <credential>, where <credential> is either a user JWT or an API
key (ug_live_... / ug_test_...). Every read on this page is open to any authenticated member of
your tenant. An embedded case view cannot reach them: an embed session is confined to its one case,
and these routes are not case-scoped.
This page covers the definitions. Reading a case’s own computed numbers is a separate surface that lands later; this guide grows to cover it when it does.
1. What a calculation pack is
Section titled “1. What a calculation pack is”An underwriting decision turns on a handful of numbers — qualifying monthly income, the housing and total expense ratios, funds to close, reserves in months, the loan-to-value family, the representative credit score, how long ago a credit event was. Every one of those is a word with a published definition, and the definitions disagree between programs: what counts as income, whose debts count, which value the ratio is taken against.
A calculation pack is that set of definitions, shipped as versioned data:
- definitions — the named quantities the pack computes, each with what it is measured in, which of the four groups an underwriter reads it under (capacity, capital, collateral, credit), how it is rounded, and the formula, which the API renders for you in plain language;
- parameters — the named figures a formula reads (a revolving-payment factor, a rental vacancy haircut, a maximum document age), each with dated values and a citation to the public guide it was taken from;
- reference tables — dated, cited lookup schedules, each row carrying its own effective range and citation;
- a selection rule — which cases the pack governs.
What a pack does not do
Section titled “What a pack does not do”A pack never supplies a threshold and never decides anything. It says what “total expense ratio” means; it does not say that 43 percent is the limit. Limits, eligibility rules and overlays come from the lender’s own guideline, through the question set that was published from it. A pack is definitions with citations, and nothing else.
It follows that packs are optional enrichment. A case for which no pack resolves, or whose inputs are incomplete, behaves exactly as it would if packs did not exist. Nothing about your integration breaks because a pack is absent, and nothing is gated on one being present.
The packs that ship today
Section titled “The packs that ship today”Six packs ship in the default line of business. One of them, base, governs any case no other
pack’s rule claims; the other five narrow it to one programme each.
| pack | governs | what it changes from base |
|---|---|---|
base |
anything no rule claims | — it is the baseline |
conventional |
conventionally financed cases | the correspondence between the computed debt ratio and the ratio an automated underwriting service reports, and a fallback that counts a share of the revolving balances on the credit report when no obligation on the file has a recorded payment |
fha |
cases insured under the federal single-family programme | a fifteen-percent gross-up on income that is not taxed; a deposit worth asking about at half of monthly income; education obligations at half a percent of balance; retirement accounts at sixty percent of value; the insurance premium taken from the published schedule when no document states one; and the three- to four-unit self-sufficiency measurement |
va |
cases guaranteed under the veterans’ programme | residual income — what is left after tax, the housing payment and the obligations — plus the residual schedule from the regulation with its state-to-region map, the guaranty fee schedule, a twenty-five percent gross-up, and education obligations at five percent of balance spread over twelve months |
usda |
cases guaranteed under the rural housing programme | education obligations at half a percent of balance where no payment is reported; revolving accounts with a balance and no reported minimum at five percent of it with no floor; retirement accounts at sixty percent of value; the same revolving-balance fallback the conventional pack carries; and six further figures — the one-year history before overtime, bonus and commission count, the ten-payments minimum, the twenty-five percent gross-up, seventy-five percent of gross rent, five percent of a revolving balance and whose income counts — re-cited to the programme’s own handbook. No guarantee fee — see below |
non_qm |
cases underwritten outside the qualified standard | qualifying income from account deposits over a statement window, less an expense share; and gross-up, education-obligation and rent factors set to this market’s common defaults. Every one of these is a pack default with no publisher |
A case above the conforming limit uses conventional. There is no separate pack for one, and
there will not be: such a case is underwritten to the same definitions and differs only in the
FIGURES an investor’s guideline names. Those arrive as confirmed parameter overrides pinned to a
published question set, not as a second set of definitions.
Each pack’s description is worth reading before you trust its numbers. It says what the pack
changes and — the part nothing else in the document can tell you — what it deliberately does NOT
contain, and why. An omitted figure omits its own evidence: there is no parameter, no citation and
no verified: false marking the place where a number was left out on purpose. Three of today’s
omissions are worth knowing about up front:
- the federal-insurance scorecard’s ratio ceilings, the two figures everyone quotes, appear nowhere in the agency’s published handbook. They ship at no value. A guideline that states them supplies them as a confirmed override;
- the rural-housing programme’s guarantee fees are in no pack. Its handbook does not state them: both fee paragraphs refer the reader to a separate exhibit of a separate instruction, and the handbook’s own worked examples say in terms that they assume a one percent upfront fee. So no fee rate ships at any value, and the annual fee reaches the housing payment only where one of your documents states it as a monthly amount — which is what that programme calls the first-year annual fee monthly amount. The same pack’s ratio guidelines (29 percent housing, 41 percent total debt, with a waiver path to 32 and 44) are requirements rather than meanings, so they live in the question set rather than in the pack;
- the guaranty programme’s residual income excludes upkeep and utilities. The per-square-foot figure lenders commonly add for them is in no published document, and the regulation requires a property-specific estimate instead. The residual number is therefore generous by whatever that estimate would have been.
2. Reading the packs
Section titled “2. Reading the packs”GET /v1/calculation-packs
Section titled “GET /v1/calculation-packs”Lists every pack the running deployment ships.
GET /v1/calculation-packs HTTP/1.1Host: api.example.comAuthorization: Bearer ug_live_...Each entry carries the pack’s identity, when it applies, a line per definition, every parameter with its dated values and citations, and a summary of each reference table:
{ "items": [ { "key": "sample_pack", "version": "2.1.0", "label": "Sample Pack", "digest": "3f1c…", // SHA-256 of the pack document "applies_to": "Applies to any case in this line of business that no other pack claims.", "synced_at": "2026-09-05T12:00:00Z", "definitions": [ { "key": "calculation.total_expense_ratio", "label": "total expense ratio", "value_type": "decimal", "unit": "percent", "group": "capacity", "rounding": "cut off at 2 decimal places and then rounded up to a whole number", "formula_version": "1.2.0" } ], "parameters": [ { "name": "revolving_payment_factor", "label": "revolving payment factor", "value_type": "decimal", "overridable": true, "values": [ { "value": "0.05", "effective": { "from": "2026-01-01" }, "citation": { "publisher": "A Publisher", "section": "S1", "url": "https://example.test/s1", "retrieved_on": "2026-09-05", "verified": true } } ] } ], "tables": [ { "name": "residual_requirement", "label": "residual requirement", "key_names": ["region", "household_size"], "value_type": "decimal", "unit": "usd", "row_count": 41 } ] } ]}vertical is absent on a pack belonging to the default line of business, exactly as it is absent
on a case that is on the default — so the two compare directly, with no special case for the empty
string.
GET /v1/calculation-packs/{packKey}
Section titled “GET /v1/calculation-packs/{packKey}”The same pack, plus what you need to explain a number rather than merely name it: each definition’s formula in plain language, the inputs it requires, the stated value it is compared against, and every reference-table row.
{ "key": "sample_pack", "version": "2.1.0", "definitions": [ { "key": "calculation.total_expense_ratio", "label": "total expense ratio", "description": "Everything owed each month, as a percentage of qualifying income.", "unit": "percent", "group": "capacity", "rounding": "cut off at 2 decimal places and then rounded up to a whole number", "formula": "total expense ratio is (the total monthly obligations calculation) as a percentage of the qualifying monthly income calculation, cut off at 2 decimal places and then rounded up to a whole number", "required_inputs": ["party.income.monthly_amount"], "compare_to": { "key": "loan.total_expense_ratio", "tolerance": "0.50", "label": "the ratio the findings report states" } } ], "tables": [ { "name": "residual_requirement", "key_names": ["region", "household_size"], "rows": [ { "keys": ["exactly west", "from 4 up to (but not including) 5"], "value": "967", "effective": { "from": "2025-01-10" }, "citation": { "publisher": "A Regulator", "section": "36.4340(e)", "url": "…", "retrieved_on": "2026-09-05", "verified": true } } ] } ]}A pack key addresses exactly one pack: two lines of business may not ship a pack of the same name. An
unknown key is a 404.
3. Reading a pack well
Section titled “3. Reading a pack well”Every decimal is a string. "0.05", "967", "0.50" — never a JSON number. Parse them with a
decimal type, not a float; a ratio that rounds differently from the published guide is a wrong answer
with a right-looking trail. This is the API-wide rule, not a rule of this surface.
A figure is only as good as its date. Every parameter value and every table row carries an
effective range: from is inclusive, to is exclusive, so two consecutive ranges cover every date
exactly once with no gap and no overlap. A figure is chosen against the case’s reference date,
never against today’s date — which is what makes a closed file’s numbers reproduce years later.
Every figure says where it came from. citation.verified is true for everything that ships
outside a test line of business: a figure nobody has confirmed against its primary source does not
load at all. A figure with no publisher is a pack default with no public source, and says so in
note — show that wherever you show the citation, because “our default” and “the agency says” are
different claims.
Concretely: a pack-default figure comes back as {"note": "Pack default — no public source. …", "verified": true} with no publisher, no url and no retrieved_on. verified: true there means
“this is deliberate and confirmed as our default”, not “an agency published it” — the absent
publisher is the whole signal, and a UI that renders only publisher will show a figure with no
provenance at all. Every parameter in the non_qm pack that differs from base is one of these,
because that side of the market has no publisher to cite: investor matrices govern, and a confirmed
guideline override replaces the default.
compare_to is an integrity check, not an override. A definition may declare the stated value it
corresponds to — a ratio an automated underwriting service reported, an amount stated on an
application, one document’s figure against another’s — and a tolerance. A disagreement beyond that
tolerance is raised for a person to look at. The stated value is never overwritten, in either
direction.
rounding is prose, and it is load-bearing. “cut off at 2 decimal places and then rounded up to a
whole number” is not the same rule as “rounded up to a whole number”: the truncation happens first,
which is why 94.01 percent becomes 95 while 80.001 percent stays 80. Render it as given rather than
paraphrasing.
digest answers “which pack is this?” It is the SHA-256 of the pack document itself. Two
deployments reporting the same pack key, version and digest are running the same definitions.
4. Versions
Section titled “4. Versions”version is the pack version, and every number computed under a pack records the version that
produced it. A pack update never rewrites a number already computed. An open case picks up a
newer pack at its next run; a completed case’s numbers stay frozen under the versions that produced
them. So a pack version you saw last month may not be the one on offer today, and both may be cited
by numbers you hold — treat the version as part of a number’s identity, not as a global setting.
formula_version is finer-grained: it identifies one definition’s formula, and moves when the
formula’s meaning moves. A re-worded label does not move it; a changed operator does.
4a. Parameters: proposals and confirmations
Section titled “4a. Parameters: proposals and confirmations”A pack’s figures are defaults, taken from a published guide and cited. Your guideline may set its own, and where it does, yours is the one we use — once a person on your side has said so.
How a proposal appears. When a question-set version is authored from a guideline, one stage of that run reads the guideline’s own statements against the pack’s OVERRIDABLE parameters and records what it found: a value, the unit your guideline wrote it in, and the sentence it came from, quoted verbatim. Nothing about that reading is in force. It is a candidate, on a list, waiting for someone.
Read the list with:
GET /v1/question-sets/{questionSetId}/versions/{version}/parametersYou get three things together, because deciding one of them needs all three: the pack’s overridable parameters (what each figure means, its unit, and the pack’s own default), every proposal the authoring run produced, and every override already confirmed for this version.
Deciding. Two routes, both requiring a person — a reviewer, admin or platform user; an API key is refused whatever its scopes, because the record of who decided is a reference to a real account and a key is not one.
POST /v1/question-sets/{questionSetId}/versions/{version}/parameters:confirm {"proposal_id": "...", "name": "...", "value": "0.15", "reason": "our guideline says fifteen percent"}
POST /v1/question-sets/{questionSetId}/versions/{version}/parameters:reject {"proposal_id": "...", "reason": "that section is about something else"}A reason is mandatory on both. Omitting proposal_id on the confirm is direct authoring: a person
who already knows their own figure does not have to wait for a run to notice it, and the result is
the same kind of record.
Publishing waits. A version cannot be published while any proposal on it is still undecided. That is deliberate: publishing freezes the rules a case will be measured against, and an open question about one of those rules would otherwise be answered quietly by the pack’s default, with no moment afterwards at which anyone noticed. Rejecting is a complete answer — the requirement is that every reading be decided, not that every reading be accepted — and a rejection takes one call.
Per-case overrides are a different thing and ride the directive mechanism, not this one: a
directive may carry {"parameters":[{"pack_key":"...","name":"...","value":"..."}]} to change a
figure for ONE case. What is described above pins a figure to a question-set version, which is to say
to every case measured against it.
What is checked. A confirmation is refused unless the pack declares the parameter, declares it
overridable, and can hold the literal you sent as its declared type in its own unit. So an override
never reaches a computation the engine cannot make sense of.
In the app. Your own people do not need any of the above: the version’s page in Underwriting Guru carries a panel under the Publish button listing every waiting reading with its figure, the pack’s default beside it, the sentence it came from and a link straight to that sentence on its page in the guideline — plus Use this figure / Leave it alone, and a form for setting a figure the guideline never mentioned. The Publish button is disabled while anything is waiting and says how many, in the same words the API would answer with. The routes above are for hosts that want to do this from their own screens.
5. Field reference
Section titled “5. Field reference”| field | notes |
|---|---|
key |
The pack’s key. Unique across every line of business, and stable. |
vertical |
The line of business. Absent for the default one. |
version |
Pack version. Recorded on every number computed under it. |
digest |
SHA-256 of the pack document, lower-case hex. |
description |
Prose about the pack as a whole: what it changes from the pack it narrows, and what it deliberately does not contain. Absent when the pack states nothing beyond its definitions. |
applies_to |
When this pack governs a case, in a sentence. |
synced_at |
When the deployment last mirrored this pack from its build. |
definitions[].key |
The key this calculation produces — always two segments under calculation.. Which instance it was computed for is carried separately, never in the key. |
definitions[].unit |
Absent for a plain number. percent, months, days, count and ratio are the engine’s own; a money unit is whatever the vocabulary calls it. |
definitions[].group |
capacity · capital · collateral · credit. Presentation only. |
definitions[].required_inputs |
The keys that must resolve for the number to be computable at all. A key the formula reads but this list omits is optional: its absence contributes nothing rather than making the whole number unknown. |
parameters[].overridable |
Whether a confirmed override may replace this figure for a question-set version (§4a). A parameter that is not overridable is a constant of the arithmetic rather than a policy — it is never proposed and never accepted. |
tables[].key_names |
What the table is looked up by, in order. |
tables[].rows[].keys |
One criterion per lookup key, in that order, in words. A criterion matching anything reads as anything. |
6. The values a calculation reads, and how they are checked
Section titled “6. The values a calculation reads, and how they are checked”A pack says how a number is worked out. The figures it works from come off the case’s documents, and
GET /v1/scenarios/{id}/facts is where you read them.
6.0a Filling the Credit section without an application file
Section titled “6.0a Filling the Credit section without an application file”The Credit section — the scores, the report date, the obligations that make up the debt ratio, the waiting period since a bankruptcy — used to need the application file, because that was the only document an adapter could read obligations out of. It does not any more.
Upload the credit response XML your bureau reseller returns, and the Credit section fills on its
own. It is a RESPONSE_GROUP document (MISMO 2.x, the file most resellers hand back beside the
printed report), it is recognised on upload with no declaration needed, and it is read the way a
structured file is read: deterministically, with no model call and no cost, straight into typed
values you can cite. Out of one file you get, for each person the report covers:
- every score with the bureau and the model that produced it, and the date the report was pulled;
- one record per account — the creditor, the account identifier, the type, whether it is open, whose it is, when it was opened and last reported, the balance, the monthly payment, the limit, the stated term, the 30/60/90 late counts and the derogatory rating;
- the inquiries and the public records, with a discharged bankruptcy’s disposition date landing on the key the waiting-period calculation measures from;
- the report’s own totals — monthly payments, revolving balances, inquiry, derogatory and public record counts — worked out from the accounts themselves rather than taken from a summary box.
Every one of those values comes back with source_format: credit-response-2.x and
review_status: not_applicable: the file states them, so there is no reading for anyone to check.
If all you have is the printed PDF, upload that instead. It is read by the extraction stage like
any other document — twice, from the text and from the page image — so its values arrive as agreed
or, where the two readings differed, as needs_review for a person to settle. The same accounts, the
same keys; a model in the loop rather than a parser, and a cost per document rather than none. The
XML is the better input whenever you have it.
One thing neither path can give you yet. A credit report states an account’s TERM and never how
many payments are LEFT, and the debt total counts a term obligation only while more than ten payments
remain. So a mortgage or a car loan known only from a credit report does not reach that total yet —
revolving accounts, leases and education obligations do. Send the application file too, or supply the
remaining count with POST /v1/scenarios/{id}/facts:assert, until that derivation ships.
6.1 Where a value comes from
Section titled “6.1 Where a value comes from”Every value carries source_format, which is either the name of the file format an adapter read it
out of, or one of two reserved words:
source_format |
What it means |
|---|---|
a format name (e.g. xml-generic, credit-response-2.x) |
read straight out of a field of a structured file |
extraction |
a model read it off a document, and you are told which passages it read |
assertion |
a person supplied it or corrected what was read |
6.2 Where a value stands
Section titled “6.2 Where a value stands”review_status is the state a value is in. Three of them mean it is settled enough to compute with:
review_status |
Meaning | Counts as a confirmed input? |
|---|---|---|
not_applicable |
read from a field of a structured file — there was no reading to check | yes |
agreed |
read twice, from the text of the page and from the picture of it, and both readings matched | yes |
confirmed |
a person read the document and confirmed it | yes |
corrected |
a person replaced what was read | yes |
auto |
only one of the two readings found it | no |
needs_review |
the two readings disagreed, a check on the figures did not add up, or it could not be attached to a party | no |
rejected |
a person said the document does not say this | no |
Filter to the queue with ?review_status=needs_review, or to one section of one document with
?logical_document_id=.
Alongside it, reading_note is one sentence saying how the value came to be what it is — “Read from
the text of the page and from the picture of it; both readings gave the same value”, “Only one of the
two readings found a value”, “A reviewer corrected this reading”. Render it as it is; it is written
for the person looking at the number, and there is nothing behind it worth reconstructing.
block_ids are the passages the value was read from, and logical_document_id is the section of the
document it was read from. A machine-read value always names at least one passage — that is a rule,
not a convention — so you can always show a reader where a figure came from.
6.3 Checking a value
Section titled “6.3 Checking a value”Four actions, all of them a person’s. An API key is refused on every one: “I read the document and this is what it says” is a claim only somebody with a name can make.
| Do this | Call |
|---|---|
| the reading is right | POST /v1/scenarios/{id}/facts/{factId}/review with {"action":"confirm","reason":"…"} |
| the reading is wrong | the same call with {"action":"correct","value":"3210.00","reason":"…"} |
| the document does not say this | the same call with {"action":"reject","reason":"…"} |
| nothing read a value you can see | POST /v1/scenarios/{id}/facts:assert |
| you know whose this is | POST /v1/scenarios/{id}/facts/{factId}/assign — see below |
The reason is required every time, and it is kept with the value.
A correction never overwrites. The machine’s reading survives with its provenance and the
person’s value takes its place as a new record — so “what did the machine read, and who changed it to
what” is answerable forever. The response tells you how many answers on the case’s last run relied on
the value that was replaced (answers_flagged); each of those is marked for a fresh look, and
nothing is re-decided on its own. Running the case again is your call.
6.3a Values not yet attached to anyone
Section titled “6.3a Values not yet attached to anyone”A value read off a document is attached to the party, account or property it belongs to by matching what the document prints against what the case already knows — the last four digits of an identifier, then the account number, then the name, then the address, each compared exactly. Nothing is ever guessed. Attaching a pay statement to the wrong person is invisible afterwards and puts one person’s figures into another person’s numbers, so when two of them match, the value is attached to neither.
Such a value comes back with provisional: true and an entity_candidates list — each one an
entity_ref and a label you can put in front of a reader (“Party 2 — Dana Whitfield”). Its
review_status is needs_review, so it is already in the count of §6.4, and its reading_note says
what happened.
No calculation reads one. A number computed from a value nobody owns would be a number about nobody, so it is left out of the inputs until somebody says whose it is.
To settle it:
POST /v1/scenarios/{scenarioId}/facts/{factId}/assign{"entity_ref": "party:2", "reason": "the second applicant on the file"}or, when neither candidate is right and this is somebody the case has not seen:
{"create_entity": true, "reason": "a third person with the same name"}One call settles the whole group. Every value that came off the same entity on the same document
moves together — you are not asked eleven times whose a bank statement is. facts_assigned tells you
how many moved, entity_ref and entity_label where they went, and answers_flagged how many
answers on the case’s last run relied on one of them and are now marked for a fresh look. Nothing is
re-decided on its own.
The values are not edited: each one is replaced by a new record carrying the entity you chose, and what the machine read survives with its provenance. A value that is already attached, or that has since been replaced, is refused rather than quietly moved.
6.4 Being told there is something to check
Section titled “6.4 Being told there is something to check”Subscribe to the extraction_review task kind with the case’s id, and you are told when a case has
values waiting — see Webhooks. The same count is on the case: GET /v1/scenarios/{id}/overview
carries needs_review_count and an extraction_review_pending entry in attention.
6.5 Documents uploaded before this existed
Section titled “6.5 Documents uploaded before this existed”Their values are not read on their own. Ask for one document to be read with
POST /v1/scenarios/{id}/documents/{documentId}/extraction; it returns immediately and the work
appears on the case’s event stream as extraction.completed, one frame per section. Send
{"reextract": true} to read sections that already have a result — a re-read replaces only what
nobody has checked, and never a value a person confirmed, corrected or recorded.
7. Transactions: reading the ledger
Section titled “7. Transactions: reading the ledger”When a bank statement is read, its transaction lines are read too, one row each:
GET /v1/scenarios/{scenarioId}/transactionsAny member of your workspace may read it, and an embed session may read its own case — the same access the case’s values have, because a ledger line is the same kind of thing.
Each entry is one line the statement printed:
{ "id": "0199...", "document_id": "0199...", "logical_document_id": "0199...", "entity_ref": "asset:1", "row_ordinal": 4, "posted_date": "2026-05-13", "description": "MOBILE DEPOSIT", "amount": "9400.0000", "direction": "credit", "running_balance": "50751.8800", "block_ids": ["0199..."], "page_no": 1, "confidence": 0.97, "review_status": "agreed", "reading_note": "Read from the picture of the page and from the lines the parser found; both readings gave the same line.", "sourcing_status": "unsourced", "created_at": "2026-05-31T09:14:22Z"}Four things about it are worth knowing before you build on it.
amount is a string and it has no sign. Every decimal on this API is a string
(api-conventions §4); this one is also always positive, and
direction — credit or debit — says which way the money moved. That is how a statement prints
it: the amount sits in a Withdrawals column or a Deposits column, and the column is the sign. Do not
infer the direction from the amount and do not display the amount without the direction.
running_balance is what the statement PRINTED on that line, or absent. It is never computed,
and it is also absent when the two readings disagreed about it — the transaction is still real, the
balance beside it is not settled.
review_status says how confident the reading is, and reading_note says it in a sentence.
agreed means two independent readings — one from the picture of the page, one from the lines the
parser found — gave the same line. auto means only one did. needs_review means they disagreed, or
a check on the statement’s own arithmetic did not add up. Show the note; it is written once, by us,
so every surface says the same thing, and it deliberately does not repeat the figures.
classification and sourcing_status are the ledger’s next chapter. A line nothing has
classified carries no classification at all and is unsourced. Classifying lines (payroll, a
transfer between the case’s own accounts, a recurring debit, a returned-payment fee, a large deposit
that needs an explanation) and recording where a deposit came from are the write actions that come
next; until then treat the absence as “nobody has looked”, not as “there is nothing to look at”.
Filtering and paging
Section titled “Filtering and paging”| parameter | effect |
|---|---|
logical_document_id |
only lines from one section of one document |
entity_ref |
only lines for one account |
from_date / to_date |
only lines posted in that range, inclusive |
classification |
only lines of one kind |
cursor / limit |
ordinary pagination — the cursor is opaque, hand it back as you got it |
Lines come back oldest first. You get the EFFECTIVE ledger: a line a person has replaced is not returned, and neither is a line an earlier reading of the same statement produced — a statement read twice shows the newest reading’s lines, once.
How a line gets sorted, and how to change it
Section titled “How a line gets sorted, and how to change it”Every line comes back with what KIND of money it is, why it was sorted that way, and who or what
sorted it. All three are ready to show a person — classification_label, classification_reason and
classification_source_label are sentences the server renders, so three different screens say the
same thing about one line. classification and classification_reason_code are the machine values
behind them, stable and safe to group by; classification_related_ids names the other lines the
decision rests on — the far leg of a transfer, the other occurrences of a repeating payment.
Sorting happens in two steps and you can tell which one answered:
deterministic— a rule matched. Transfers are paired across the case’s OWN accounts by amount and date, pay is matched to an employer the case knows or to its recorded pay cycle, repeating payments and bank charges are matched by wording, and a deposit nothing else explains is flagged as a large one when it reaches the size the case’s rules say to ask about.model— no rule fitted, so the line was sorted on what its description says. The choice is from a closed list, andlarge_depositis not on it: whether a deposit is large depends on a threshold this step is never shown.human— somebody said so.
POST /v1/scenarios/{scenarioId}/transactions/{transactionId}/classify (reviewer, signed-in
person only) changes it. Send the classification and a reason; the reason is mandatory and is
kept. A line nobody had sorted takes your answer directly. A line that was already sorted is
REPLACED — your answer lands on a new line carrying the same reading, the old one is marked as
replaced, and the reply’s replaced_transaction_id tells you which. Nothing is overwritten, and
reading the statement again later will not undo what you said.
POST .../transactions/{transactionId}/sourcing answers the question a large deposit asks. Send
sourced with a note saying where the money came from, or not_required. A line leaves
unsourced once.
The totals
Section titled “The totals”GET /v1/scenarios/{scenarioId}/transactions/summary gives you, per account and statement
period: money in, money in with movements between the applicant’s own accounts taken out, pay, how
many large deposits there are and how many still need an explanation, how many returned payments, and
the balances — opening and closing as the statement printed them, plus the ending balance from the
last line and an average daily balance. It also gives the case’s large_deposit_threshold.
Every decimal is a string. A figure that is absent is absent, not zero. An average daily balance over a statement that prints a balance on only some lines is not computable, and reporting zero for it would be a measurement nobody made. The same goes for the threshold: it is absent when no set of rules governs the case, which is a different statement from “nothing crossed it”.
These figures are also promoted as the case’s own values, and it is those the calculations read — never the lines themselves. That is what makes a number a decision stands on traceable to the lines that made it rather than to a formula somebody would have to re-run.
One of them reaches the decision itself. When an answer fails on a question about those deposits, the
case’s conditions gain an unsourced_funds item reading “Deposits with no stated source for
What is not here
Section titled “What is not here”A statement past its line limit produces no ledger rather than a partial one, and says so on the document’s extraction result. That is deliberate: a ledger missing its last forty lines looks exactly like a ledger with forty fewer transactions, and anything you totalled over it would be wrong with nothing to tell you.
8. Reading a case’s numbers
Section titled “8. Reading a case’s numbers”Sections 1 to 6 are about the definitions — what a figure means and where the meaning came from. This section is about one case’s numbers: what came out, from what, and what you can do about it.
8.1 The whole worksheet in one read
Section titled “8.1 The whole worksheet in one read”GET /v1/scenarios/{scenarioId}/calculationsAny authenticated member of the workspace may read it, and so may an embedded session bound to that case. One read is the whole worksheet. Everything needed to explain a number travels inline — the figures used and who published them, the values read down to the document and page, the dependency graph in both directions, the labels for every key and every party — so a read-only viewer needs no second call and no ability to make one.
That promise is what the embedded view is built on: if you frame a case in your own product
(see the embedding guide), the frame’s Analysis tab renders this same worksheet — the four sections, the
drill-down, the citations and the jump to the page a value was read from — with nothing on it that
writes. Post { tab: "analysis" } to land your reader there. The same numbers also reach the
findings report as its own Calculations section, worked out from the set the decision was taken
against rather than from the case’s current numbers, so a report re-rendered after a recompute never
disagrees with the copy already filed.
The envelope tells you about the case:
| Field | What it is |
|---|---|
pack_key, pack_version, pack_label |
The set of definitions the stored numbers were worked out under. Absent when none governs the case — an ordinary state, not an error. |
latest_pack_version, newer_pack_version_available |
Whether a newer version of the same set exists. A pack update never rewrites a stored number: an open case picks it up at its next document, run or explicit recompute. |
reference_date, reference_date_set, as_of |
The date every dated figure is measured against, whether it is the case’s own, and the date the stored numbers were actually evaluated as of. |
frozen |
True once a person has accepted the case’s decision. Its numbers have stopped moving, and overrides and recomputes are refused. |
recompute_available |
Whether working the numbers out again could do anything. |
needs_review_count |
How many NUMBERS are waiting on a person: those resting on a value nobody has checked, and those whose figure disagrees with a figure a document states. Note that ScenarioOverview.needs_review_count is a different quantity — it counts VALUES — which is why the two fields below exist and say what they count in their own names. Neither is deprecated. |
unconfirmed_input_count |
How many numbers could not be finished because a value they read is still waiting on a person. A subset of needs_review_count. |
values_awaiting_check_count |
How many VALUES read out of this case’s documents are still waiting on a person, whether or not any number reads them. The same count as ScenarioOverview.needs_review_count, read through the same query and carried here so a worksheet can say what is waiting without a second request. Bind this one to a “something is waiting” badge or checklist; bind needs_review_count only where you mean the numbers themselves. |
integrity[] |
Where a figure we computed and a figure a document states are further apart than the tolerance allows. Both are shown; neither is corrected. |
preview |
Always false here. See §7.5. |
Each item in items[] is one number:
| Field | What it is |
|---|---|
id |
The row. It is what an answer cites when a computed value drove a check. |
key, label, description, group |
The vocabulary key and its human name; group is the worksheet section (capacity, capital, collateral, credit on the shipped sets — data, so the set can grow). |
entity_ref, entity_label |
Who or what it is about. entity_ref is empty for the number that belongs to the case rather than to an instance in it; entity_label is always a phrase a person can read, never an ordinal — the same rendering the extraction review uses for the same entity. |
status, status_label |
computed, unconfirmed_inputs, insufficient_inputs or overridden, and the one wording every surface uses for it. |
value, value_type, value_unit, value_places |
The figure, as a string. A decimal never crosses this API as a JSON number, and value_places is the scale it was computed to — state it at that scale. It is the scale of the RESULT, not of any step inside the rounding: a ratio rounded up to the whole percent by the agency convention carries value_places: 0 and is stated 80%, even though the rule truncates at two decimals on the way. |
formula_text |
The whole formula in words. |
inputs[] |
Every value read, with the value as it was read, its review status, and the document, section, page, passages and path it came from. An input whose value is one of a fixed set of tokens also carries value_label — the token in the reader’s words, charge_off as Charged off. Show value_label wherever it is present and never render the token. |
parameters_used[], tables_used[] |
The figures and schedule rows it consulted, each with the range it applies over and its citation. A figure with no publisher carries its own disclosure instead. |
exclusions[] |
What it left out on purpose. reason is always an engine code; reason_label carries the set’s own sentence when it gave one. A reason of duplicate_of:<entity_ref> names the entity that was counted instead — see below. |
missing[] |
What it needed and did not have, with a label so a checklist reads in words. |
depends_on[], dependents[] |
The graph in both directions. dependents[] is what an override changes. A dependency’s entity_ref may be EMPTY on a line that is itself about an instance: that is the case-wide figure the line was measured against — an obligation’s payment tested against a share of the household’s income, say — and it is a real edge, so overriding the case figure moves those lines too. |
compare_to |
The stated figure this one corresponds to, and whether the two agree. |
override |
The value a person recorded in its place, with their reason. |
8.1a One obligation reported by two documents
Section titled “8.1a One obligation reported by two documents”Two documents can each describe the same real-world thing. An application file lists an obligation from what the applicant wrote down; a credit report lists the same obligation from what the creditor reported. Each document is read on its own, so the case ends up carrying two entries for one debt, and a total that counted both would overstate it.
The calculation set decides when two entries are the same thing, and it says so instead of quietly
merging them. Two obligations agreeing on the last four digits of the account number and on the
holder’s name are one obligation. The one read first is the one that counts; the other is left out,
and appears in exclusions[] with the reason duplicate_of: followed by the entity_ref that was
counted in its place:
{"entity_ref": "liability:9", "key": "liability.monthly_payment", "value": "1642.00", "reason": "duplicate_of:liability:1"}Nothing is deleted and nothing is blended. Both entries stay on the case with their own documents and their own figures, and if the two documents disagree about the balance, neither figure is overwritten — the difference stays visible, because choosing between them is a person’s decision. An obligation missing either identifier is never folded into another: an absent account number is not evidence that two debts are the same one.
8.2 How one number got where it is
Section titled “8.2 How one number got where it is”GET /v1/scenarios/{scenarioId}/calculations/{calculationKey}/history?entity_ref=party:1Every version the number has held, oldest first, cursor-paginated. Each entry says what it was
worth, what status it carried, and — in change — what that version was: the first computation,
a value a reviewer recorded, a fresh computation, or what is left after the evidence behind it was
destroyed. Omit entity_ref to trace the case-wide number.
A version leaves the live set two ways, and current: false alone does not say which: superseded_by
names the row that replaced it, and retired_reason says the replacement stopped producing it at all —
absent_from_replacement, entity_removed, definition_changed or inputs_replaced (R15, migration
00099). retired_reason is absent on every version that is still current or was superseded instead;
change always carries the sentence, retired_reason is what a client styles or filters on without
parsing it.
Nothing here was ever updated in place, so the chain is the record.
8.3 Recording a value in place of a computed one
Section titled “8.3 Recording a value in place of a computed one”POST /v1/scenarios/{scenarioId}/calculations/{calculationKey}/override{"entity_ref": "party:1", "value": "4850.00", "reason": "The newest payslip supersedes the application."}Reviewer or administrator, and a person — an API key is refused, because an override is a claim with a name against it. The reason is mandatory: it is kept with the value and appears wherever the number is explained, including the report.
The computed value is not destroyed. It is superseded by a row carrying the person’s value, their
reason and their name, and the record of what was read to produce the number it replaced travels
with it. Every number that reads the overridden one is worked out again with the asserted value in
the arithmetic, and the response names them in dependents_recomputed so you can show what moved
without a second read. answers_flagged counts the answers on the case’s last run that cited a
number that moved; they are flagged for a fresh look and nothing is re-decided on its own.
An override stands until one of the values behind it changes. A recompute over the same evidence
leaves it exactly where it is; a corrected value, a new document or an erasure ends it and the
computed figure returns. A case whose decision has been accepted is frozen and the override is
refused with 409.
8.4 Working the numbers out again
Section titled “8.4 Working the numbers out again”POST /v1/scenarios/{scenarioId}/calculations/recomputeReviewer or administrator. It never fails for a business reason: when nothing was written the
response says why in skipped (no_pack, frozen, unchanged) and says it in a sentence in
skipped_detail. None of the three is a fault.
Numbers are also worked out again on their own when a document finishes being read, when the case is
re-evaluated, when a case is copied to another programme, and when the case’s as-of date moves
(PATCH /v1/scenarios/{scenarioId} with reference_date). Every recompute that writes something
puts a calculations.recomputed frame on the case’s event stream
(GET /v1/scenarios/{scenarioId}/events), so an open worksheet can refresh in place.
Every human act on a value read out of a document — a confirm, a correction, a rejection, a value a
person asserts, an assignment of an unattached group to a party or account — puts a
fact_review.recorded frame on that same stream, carrying
{scenario_id, fact_id, action, facts_affected, answers_flagged}. Both frames carry identifiers,
enums and counts and never a figure, so the response to either is to re-read. Subscribe to both if
you show a case’s numbers to somebody: two people reviewing one case will otherwise each see only
their own progress.
8.5 Trying a different value, without recording anything
Section titled “8.5 Trying a different value, without recording anything”POST /v1/scenarios/{scenarioId}/calculations/preview{"substitutions": [{"kind": "fact", "key": "party.employment.period_base_pay", "entity_ref": "party:1/employment:1", "value": "6000.00"}]}Reviewer or administrator, and a person. It runs the same engine over the same values with your
substitutions applied in memory and returns the same shape as §8.1, marked preview: true, plus
changed[] — the numbers whose value or status differs from what is on file, each carrying what it
is now and what it would be.
The same values include the figures somebody has already asserted. A preview starts from the
case as it stands: a number carrying a recorded override (§8.3) keeps that figure, and everything
reading it is worked out from the figure on file rather than from the one the definitions would have
produced. Substitute that number’s own key and your substitution wins — for the preview only — and
the key appears in changed[] with previous_status: "overridden" and the recorded figure as
previous_value, so you can tell the reader which assertion their question is standing in for.
Nothing about an override is ever ended by a preview, however far the hypothetical moves its inputs.
A value still waiting to be attached to somebody cannot be substituted, and the request is refused
rather than quietly ignored: a hypothetical about nobody is worse than no hypothetical. Attach it
first with POST /v1/scenarios/{scenarioId}/facts/{factId}/assign.
It writes nothing. No number, no override, no answer, no citation. Preview items carry no id,
because there is no row: a hypothetical is not something an answer can ever cite, and the report
never mentions it. The only trace is one audit entry per request naming the case, the set of
definitions and the keys you substituted — never the values you tried and never the results.
Three kinds of substitution, matching what a person can override:
kind |
Substitutes |
|---|---|
fact |
A value read from a document — every value under that key for that instance. A key the case has no value for is added, so “what if their income were 6,000” works on a case that states no income. |
calculation |
A number on the worksheet. The numbers that read it follow it. Must name a number the case currently holds. |
parameter |
A figure the definitions use. |
Leaving the sandbox is your client’s business: discard it, or hand the value to §8.3’s override, where it becomes an ordinary recorded assertion with a reason you still have to write.
8.6 What the report carries
Section titled “8.6 What the report carries”A findings report pinned to a decision carries a Calculations section: the groups with their figures and status, each number’s formula in words and the values it read with their document and page, the values a reviewer recorded with their reasons, the disagreements, and what was still needed. It reads the numbers pinned to that run, not the case’s current ones, so a report re-rendered after a recompute cannot disagree with the copy already filed.
Related
Section titled “Related”- Quickstart — the shortest path from a credential to a running underwrite.
- Webhooks — being told when a background task finishes.
- Embedding a case — putting one read-only case inside your own page.
- API reference — every route, request and response.