Skip to content

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.

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.

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.

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.

Lists every pack the running deployment ships.

GET /v1/calculation-packs HTTP/1.1
Host: api.example.com
Authorization: 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.

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.

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.

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}/parameters

You 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.

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.

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

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.

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.

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.

When a bank statement is read, its transaction lines are read too, one row each:

GET /v1/scenarios/{scenarioId}/transactions

Any 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”.

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, and large_deposit is 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.

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 total , and nothing on this case says where the money came from.” It states the amount and stops — what to do about it is the guideline’s requirement, not ours — and it goes away on its own as each deposit is sourced, with nothing to retract.

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.

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.

GET /v1/scenarios/{scenarioId}/calculations

Any 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.

GET /v1/scenarios/{scenarioId}/calculations/{calculationKey}/history?entity_ref=party:1

Every 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.

POST /v1/scenarios/{scenarioId}/calculations/recompute

Reviewer 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.

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.

  • 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.