Spreads

Finalized figures as data: the spread lifecycle and the attribute payload.

What a spread is

A spread is the analyst’s finished read of a borrower’s documents on one loan. It lays the returns, statements and schedules out as the figures a credit decision is made on. It is worked in the SpreadSpace workspace or inside your own product through the embed.

When the analyst finalizes it, those figures are frozen as an attribute snapshot: a versioned, machine-readable statement of the loan’s numbers, keyed by name and period. That snapshot is what your backend reads. Only the finished figures, and what they were built from, cross the API. The spread’s own layout, formulas and working state never do.

The lifecycle

While the analyst works, saves create draft snapshots. A draft moves only when the figures move: a save that leaves the payload unchanged creates nothing.

Finalize is the key step. It creates the machine and final snapshots at consecutive per-loan versions, locks the board on the final snapshot, and sends the spread.finalized event. A locked board refuses replace, delete and a second finalize alike with 423 spread_locked until it is reopened.

The spread.finalized event is a pointer. It carries the loan, the board, both snapshot ids and the version, but not the figures. Fetch those with the reads below. See Webhooks for the envelope and delivery rules.

Reopen clears the lock so the board can be edited again, bumps its version, and queues the spread.reopened webhook. That event points at the final snapshot that was current, so a backend holding a copy can mark it stale. The snapshots created by the finalize are kept as the loan’s history. The next finalize creates a new version alongside them instead of replacing them.

Statuses

StatusWhat it is
finalWhat an analyst approved. Created at finalize, frozen from then on, and the one to build on.
machineThe same spread built from the documents alone, with no analyst input. Created alongside final.
draftThe latest autosave of a spread still being worked on, so it moves.

Reading attributes

Three reads, all requiring the spreads:read scope, all in the Spreads group of the API reference:

OperationReturns
GET /api/loans/{loanId}/attributesThe loan’s latest snapshot of one status. ?status=final (the default), draft or machine. The envelope is constant; snapshot is null when the loan has no snapshot of that status yet.
GET /api/loans/{loanId}/attributes/versionsThe snapshot history across every status, newest first: ids, statuses, versions and timestamps, but not the figures.
GET /api/loans/{loanId}/attributes/{snapshotId}One snapshot by id, with its payload and manifest. Use it to read a version that is no longer the latest of its status, and to read the snapshot an event names. It answers the snapshot itself: there is no { loan_id, snapshot } wrapper on this read, because a by-id read is never empty.

The versions list is capped at the 200 most recent snapshots and is not paginated; fetch any one of them by id for its payload and manifest. Add ?status=final (or draft, machine) to filter it to one status. The first row is then the current final, so backfilling a deal whose finalize predates your subscription is one call rather than listing and then filtering. The 200-row cap applies after the filter.

Every snapshot and summary carries snapshot_id, version (per-loan, counting up across every status), status, created_at, and spread_board_id when one is known. On a final or machine snapshot, finalized_at says when the analyst finalized and finalized_by names them; both are null on a draft, and finalized_by is also null for a caller who may not see names, and a plain API key never does. The join key a backend wants is finalized_by_external_user_id: the external_user_id your own embed session named for that analyst, returned to every caller because it is your identifier, not a person’s name. It is null on drafts and when the finalize happened in the SpreadSpace workspace or under a session that named no user.

A snapshot id belonging to another tenant or another loan answers 404, never 403. The read never confirms that a foreign id exists.

An API key receives values only. The source-provenance geometry our own surfaces draw click-to-source highlights from is stripped out of the payload and the manifest at every depth for a key-authenticated caller.

The payload

The payload states the figures. schema_version is attributes.v1.

KeyShape
schema_version"attributes.v1".
periodsThe fiscal-year labels the annual figures are keyed over, ascending.
attributesName → { label, unit, basis?, values, documents }. values maps a period key to the figure or null; documents maps the same keys to the documents that period’s figure was read from.
ratiosName → { label, unit, basis, values }. Ratios cite no documents: each is stated over the attributes in the same snapshot.
inputsWhat the analyst applied: addbacks (each with its printed label, whether it adds or subtracts, the years it was applied to and the amount per year), dscr_configs (each recipe’s name and the label of its numerator), hidden_rows, and custom_visuals, the count of analyst-built figures placed on the spread.

unit is money, ratio, percent or count. A money attribute read from a filed return carries a basis (tax or book) naming which side of the return the figure came from; a ratio always carries one, saying where its operands came from. Every value basis can take is listed under Basis on the catalog page. An attribute keyed by as-of date rather than fiscal year (an aging, a debt schedule) carries period_kind: "as_of_date".

Values follow the house value conventions: money is a string with exactly two decimals ("754144.00", "-5644.00"), ratios and percents are strings with at most four decimals, counts are integers, and timestamps are RFC 3339 UTC. A period a figure has no value for reads null. Keys are never dropped, so an absent year is visible as absence rather than as a missing key.

The manifest

The manifest states what the payload was built from.

KeyShape
documentsEvery document the figures were read from: document_id, document_type, fiscal_year, period_end, extraction_version.
inputs_summaryCount-level totals of the analyst inputs: addbacks, dscr_configs, hidden_rows, custom_visuals.
entityThe spread’s entity scope the figures were derived under (key and name), when known.
built_atWhen the payload was built. RFC 3339 UTC.
builder_versionThe build that produced the payload, null when the build states none.

A snapshot is content-hashed over its payload. When a new snapshot would repeat the latest snapshot of that status byte for byte, no new version is written. Two identical payloads from two different builds dedupe to one snapshot, and builder_version says which build produced the one that was stored. A manifest field does not change the hash.

The catalog

The fixed vocabulary is generated from the table the builder emits through and published as the attribute catalog. It lists every attribute and ratio name a snapshot can carry, with its label, unit, basis and the conditions it appears under. A name there is a name you can key on.

Two families cannot be listed there, because they are named after things an analyst typed:

  • dscr_<recipe> is one ratio per debt-service coverage recipe saved against the business on the loan. The slug is the recipe’s own name, lowercased, with each run of characters outside a-z0-9 collapsed to a single _, the edges trimmed and the result capped at 48 characters; two recipes that slug the same are suffixed _2, _3 … in the order the loan holds them. inputs.dscr_configs names every recipe saved on the loan, whether or not it had a figure to state.
  • custom_<slug> is one entry per figure an analyst built and placed on the spread. Same slug rule, trimmed again after the cap so a key can never end in _, and an empty result reads visual. A slug is claimed once per snapshot across both groups, with repeats suffixed _2, _3 … A figure that divides is stored in ratios. One that does not is an aggregate money figure and is stored in attributes. Each carries custom_visual_id, the definition’s own opaque id, stable across renames. Key on the slug and you accept rename churn. Key on the id and you do not.

Both families appear in final and draft snapshots, because each is analyst judgment applied on every save. Neither appears in the documents-only machine snapshot. What an analyst built a figure out of never leaves the workspace.

Provenance

Provenance in an attribute snapshot is document-level: for each attribute, each period’s documents entry lists the document_id and extraction_version its figure was read from, and [] when the figure is null. That is enough to tell a reader which filing a number came from, and to notice when a document has been re-extracted since.

Sub-document geometry (where on the page a figure was found) is not part of this surface. It reaches the SpreadSpace UI, which draws click-to-source highlights from it, and is stripped for an API key at every depth of both the payload and the manifest.