> This page is for API.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.spreadspace.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.spreadspace.app/_mcp/server.

# Spreads

## 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](/embed/overview).

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](/api/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

| Status    | What it is                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------- |
| `final`   | What an analyst approved. Created at finalize, frozen from then on, and the one to build on.      |
| `machine` | The same spread built from the documents alone, with no analyst input. Created alongside `final`. |
| `draft`   | The 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](/api):

| Operation                                           | Returns                                                                                                                                                                                                                                                                                         |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/loans/{loan_id}/attributes`               | The 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/versions`      | The 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`.

| Key              | Shape                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema_version` | `"attributes.v1"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `periods`        | The fiscal-year labels the annual figures are keyed over, ascending.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `attributes`     | Name → `{ 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.                                                                                                                                                                                                                                                                                                                          |
| `ratios`         | Name → `{ label, unit, basis, values }`. Ratios cite no documents: each is stated over the attributes in the same snapshot.                                                                                                                                                                                                                                                                                                                                                                                           |
| `tables`         | Name → `{ 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. |
| `inputs`         | What 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](/api/attributes-catalog#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](/api/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.

| Key               | Shape                                                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `documents`       | Every document the figures were read from: `document_id`, `document_type`, `fiscal_year`, `period_end`, `extraction_version`.               |
| `inputs_summary`  | Count-level totals of the analyst inputs: `addbacks`, `dscr_configs`, `hidden_rows`, `custom_visuals`, `custom_tables`.                     |
| `entity`          | The spread's entity scope the figures were derived under (`key` and `name`), when known. A loan-level snapshot states the key `whole-loan`. |
| `built_at`        | When the payload was built. RFC 3339 UTC.                                                                                                   |
| `builder_version` | The 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](/api/attributes-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](/api/attributes-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.