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

# Credit memos

A credit memo is the document: the underwriting write-up an analyst authors and
saves in SpreadSpace, in the workspace or inside your product through the
[embed](/embed/overview). The [attribute snapshot](/api/spreads) is the data
from the same spread. One loan produces both: the figures your system keys on
and the narrative a credit file needs.

This surface hands out **metadata and finished artifacts only**. A memo's
contents are never returned through the API; what you can fetch is the list,
the render state, and a link to the rendered PDF.

## The loop

Five operations, in the Loan Credit Memos group of the
[API reference](/api): list the memos, request a render, poll a render,
cancel a render, and fetch the PDF link. The list, the render read and the
PDF link need the `memos:read` scope; requesting or cancelling a render needs
`memos:render`.

| Step                       | Operation                                                              |
| -------------------------- | ---------------------------------------------------------------------- |
| 1. List what exists        | `GET /api/loans/{loan_id}/memos`                                       |
| 2. Ask for a PDF           | `POST /api/loans/{loan_id}/memos/{memo_id}/renders`                    |
| 2a. Poll the render        | `GET /api/loans/{loan_id}/memos/{memo_id}/renders/{render_id}`         |
| 2b. Cancel a queued render | `POST /api/loans/{loan_id}/memos/{memo_id}/renders/{render_id}/cancel` |
| 3. Fetch the link          | `GET /api/loans/{loan_id}/memos/{memo_id}/pdf`                         |

**List** returns each memo's `id`, `name`, `updated_at`, `finalized_at`,
`render_state`, and, for a finalized memo, the figures it was approved on
(`spread_snapshots` and `figures_status`, below):

| `render_state` | Meaning                                                |
| -------------- | ------------------------------------------------------ |
| `none`         | Never rendered.                                        |
| `rendering`    | A render is in flight.                                 |
| `ready`        | The latest PDF matches the memo as saved.              |
| `stale`        | The memo was edited after its latest PDF was rendered. |

**Render** answers `202` with the render. Poll it on
`GET /api/loans/{loan_id}/memos/{memo_id}/renders/{render_id}` until it reaches a
terminal status; when it succeeds, the PDF is available at step 3, and the
render's `links.pdf` points there. Every request renders: the PDF carries the
loan's figures as they stand when it is printed, so an unchanged memo is still
rendered fresh. A render can be cancelled while it is queued
(`POST .../renders/{render_id}/cancel`); once it is running it finishes in
seconds, and a cancel then answers `409`.

**PDF** returns a short-lived download link: `url`, `expires_in_seconds`,
`rendered_at`, `stale`, and `final`. `expires_in_seconds` is a guaranteed-valid floor
of 300 seconds; the link itself lives five to ten minutes. Request again for a
fresh one. Until a render has finished, this operation answers
`404`. That is the normal state of a memo nobody has rendered, not an error to
retry against.

`stale` on that response means the same thing as in the list: the memo was
edited after this PDF was rendered. The link still resolves to the older PDF,
so decide whether to pull it or request a fresh render first.

## Render after finalize

Render once the spread is finalized. Finalizing freezes the loan's figures as
the `final` attribute snapshot and locks the board, so a memo rendered after
that point is a PDF of numbers that can no longer move underneath it. Render
against an open board and the analyst may change a figure a minute later, with
nothing in the PDF to say so.

A render is not pinned to a snapshot version: the PDF records what the memo
said when it was rendered, and `rendered_at` is what dates it. If you need the
figures themselves tied to a version, that is what the attribute snapshot's
`version` and `finalized_at` are for.

## Finalized memos

A memo finalized in SpreadSpace is locked, and its final PDF is the document of
record. The list carries `finalized_at` on that memo; it is null while the memo
is still open. `GET /api/loans/{loan_id}/memos/{memo_id}/pdf` then returns that
final PDF with `final: true` and `stale: false`, and keeps returning it however
much the loan changes afterwards. While the final PDF is still being made, or if
it could not be made, the same call answers `404`.

`render_state` on a finalized memo describes the final PDF: `rendering` while it
is being made, `ready` once it is made, and `none` if it could not be made. It is
never `stale`.

Requesting a render of a finalized memo answers `423` with `memo_locked` and the
memo's finalize time in `details.locked_at`. Reopen the memo in SpreadSpace to
render it again.

Two webhook events follow the same pair. `memo.finalized` is sent when the final
PDF is ready to pull, and `memo.reopened` is sent when the memo is reopened for
editing. Both carry ids and the `spread_snapshots` below, never the memo or a
figure, so pull the PDF from the endpoint above; see
[Webhooks](/api/webhooks) for the envelope and delivery rules.

## The figures a memo was approved on

A finalized memo names the spread figures it was approved on. `spread_snapshots`
carries one entry per entity the memo's figure exhibits cite: the `final`
attribute snapshot (`snapshot_id`), its `version`, and the `entity` (`key` and
`name`). Read the figures themselves by id with
`GET /api/loans/{loan_id}/attributes/{snapshot_id}`. An open memo carries no
entries, and a finalized memo with no figure exhibits carries an empty list.

Finalizing a memo freezes those figures. An entity whose spread is already
finalized is cited on that spread's `final` snapshot. An entity whose spread is
still open is finalized together with the memo: its `final` and `machine`
snapshots are created at the loan's next versions, and the analyst inputs
behind its figures (company-reported add-backs, EBITDA configurations and DSCR
recipes) refuse changes with `423 spread_locked` until the memo is reopened.
The loan's add-back sets, and the company-reported add-backs kept for the whole
loan, are held for as long as the memo is finalized, the same as while any
spread on the loan is finalized. Reopening the memo releases what the memo
finalized; an entity it only cited stays held by the spread that finalized it.

`figures_status` says whether those figures still stand: `unchanged`, or
`moved` once a spread the memo cites is reopened, a document its figures were
read from is re-extracted, a document is added to the loan after the memo was
finalized, or the workspace's chart of accounts changes. It is null while the
memo is open. The final PDF stays the document
of record either way; `moved` says the numbers behind it are no longer the ones
on the loan.

## One render at a time

Renders for a workspace run one at a time. While one is in progress a second
request is refused with `429` and `Retry-After: 10`, and the refusal names the
in-flight render in `render_id`. Wait and retry, or poll that render.
A typical render takes seconds.

## Webhooks

Three events report the end of a render: `memo_render.succeeded`,
`memo_render.failed` and `memo_render.cancelled`. Each carries the render,
memo and loan ids plus a timestamp saying when it reached that state, and
`memo_render.failed` adds a `reason`. None of them carries the PDF: on a
success, fetch the link from the PDF endpoint above. See
[Webhooks](/api/webhooks) for the envelope and delivery rules.

`reason`, and `error_code` on a failed render, is one of `memo_unreadable`
(the memo could not be loaded: save it again and retry), `render_timeout`
(the render took too long), `navigation_failed` or `render_unavailable` (the
render could not start), `stale_running_exceeded` (the render stopped making
progress and was abandoned), or `internal_error`. Request the render again
after any of them.