Skip to navigation

Spreads

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

What a spread is

A spread organizes a borrower’s financial information for a loan. Analysts review it in the SpreadSpace workspace or in your application through the embed.

Finalizing a spread creates an attribute snapshot with the approved figures, periods, and source-document references. Your backend retrieves this versioned record through the API. Workspace layouts and formula definitions are not included in the snapshot.

The lifecycle

Saving changes creates a draft snapshot. Saves with unchanged values do not create another snapshot.

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. Finalize also locks the analyst inputs behind that board’s figures until the board is reopened: a change from any spread on the loan answers the same 423 spread_locked. Company-reported add-backs, EBITDA configurations and DSCR recipes are held per entity, for the entity the board was finalized under. The loan’s add-back sets, and the company-reported add-backs kept for the whole loan, are held while any spread on the loan is finalized. A board finalized under no entity holds every scope on the loan.

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/{loan_id}/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/{loan_id}/attributes/versionsThe snapshot history across every status as { loan_id, snapshots }, newest first: ids, statuses, versions, timestamps and each snapshot’s entity scope, but not the figures.
GET /api/loans/{loan_id}/attributes/{snapshot_id}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 integration is one call rather than listing and then filtering. The 200-row cap applies after the filter.

Each row also carries entity: the key and name the snapshot’s manifest states, or null when it states none. On a loan that carries more than one business or guarantor, that is how the rows are told apart without fetching each one.

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. finalized_at is set on a final snapshot and is null on machine and draft; finalized_by is set on final and machine, null on a draft, and 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.

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.
tablesName → { label, custom_visual_id, label_column, columns, rows }, one entry per custom table placed on the spread, {} when none is placed. custom_visual_id is the table’s own id, stable across renames. label_column is the head of the label column, or null when it is blank. Each column is { label, kind }, kind being figure or text. Each row is { label, cells }, one cell per column as displayed on the spread, null for a blank cell. Cells are display strings, not parsed figures.
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, custom_visuals, the count of analyst-built figures placed on the spread, and custom_tables, the count of custom tables 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 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, custom_tables.
entityThe spread’s entity scope the figures were derived under (key and name), when known. A loan-level snapshot states the key whole-loan.
built_atWhen the payload was built. RFC 3339 UTC.
builder_versionThe build that produced the payload, null when the build states none.

A payload matching the latest snapshot of the same status does not create another version. Changes to manifest metadata alone do not create another snapshot. builder_version is informational and may be null.

The catalog

The attribute catalog lists supported attribute and ratio names, labels, units, bases, and availability conditions.

Three groups use names based on labels an analyst typed or a return printed:

  • dscr_<recipe> identifies a debt-service coverage ratio saved for the business on the loan. inputs.dscr_configs lists the configured recipes, including those without a result.
  • custom_<slug> identifies an analyst-defined figure. Ratios appear in ratios; aggregate money figures appear in attributes. Use custom_visual_id as the stable identifier across label changes.
  • personal_return_schedule_c_<business>_<line> identifies one Schedule C business on a guarantor’s return, named after the business as printed. The fixed personal_return_schedule_c_<line> names carry the sum across the return’s businesses only when the return prints two or more.

These names are normalized from their labels, with numeric suffixes where needed to distinguish duplicates. Read the returned keys instead of constructing them. The attribute catalog documents the naming rules.

Both groups appear in final and draft snapshots. They are not included in the documents-only machine snapshot. Their formula definitions remain in the workspace.

A statement whose revenue is rent alone reads as a rental property statement, and its snapshot then carries company_reported_noi. Presence of that name is the designation: a statement with other revenue beside its rent states company_reported_rental_income and no NOI. A snapshot finalized under a guarantor rather than a business carries the personal_return family: the Schedule E rental figures and, when the return files Schedule C, each business’s income block, total expenses and net profit under its own name, with the sum across businesses when the return prints two or more. Company-reported figures from statements attached to that person sit beside them; manifest.entity names the scope.

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.

Use the workspace or embedded review panel to view a figure’s location in the source PDF.