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

# Statement mapping

> **Note**
>
> The mapping covers profit and loss statements. Reading one requires a plan that
> includes statement mapping; other plans get `403` with
> `statement_mapping_not_in_plan`.

## What a mapping is

A mapping is one profit and loss statement placed on the workspace's chart of
accounts. Every line the statement prints is filed on an account, every section
and account carries its figure per period, the headline measures (gross profit,
operating income, income before income taxes, net income) are stated, and every
figure says where in the document it comes from. The body is a contract you can
check: the tree adds up to the cent by the rules under
[The arithmetic](#the-arithmetic), and what it adds up to is the bottom line
the statement prints.

For every usable profit and loss statement a mapping is prepared after
extraction. It is read by document id, or listed per borrower, in the Statement
Mappings group of the [API reference](/api). Both reads need the
**`extractions:read`** scope.

| Operation                                                     | Returns                                                                                                                                                                                            |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/borrowers/{borrowerId}/extractions/{docId}/mapping` | One document's latest mapping, the [Statement Mapping object](/api/api-reference/statement-mappings/object).                                                                                       |
| `GET /api/borrowers/{borrowerId}/extractions/mappings`        | A borrower's mappings, one per document, newest document first, as `{ data, limit, next_cursor, pending_document_ids }`. `?loanId=` narrows it to one loan; `limit` is 1 to 25 and defaults to 10. |

```bash
curl https://api.spreadspace.app/api/borrowers/4a2c8e1f6b3d5a7c9e0f1b2d/extractions/7a1d4e9c2b3f4a5d8e6c1b0f/mapping \
  -H "Authorization: Bearer ss_live_..."
```

The response carries the mapping's identity and versions on the envelope and
the statement itself in `payload`:

| Key                                             | What it says                                                                                                                                                                             |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mapping_id`                                    | The mapping's id. A statement gets a new mapping, under a new id, whenever its extraction, the workspace's chart of accounts, caption rules or export map, or the mapping logic changes. |
| `status`                                        | `ready` or `unmappable`.                                                                                                                                                                 |
| `payload.mapping_state`                         | On a ready mapping, `mapped` or `unfooted`.                                                                                                                                              |
| `extraction_version`                            | The revision of the document's extraction the mapping was made from, the same `extraction_version` the document list and the document webhooks carry.                                    |
| `chart_document_version`, `chart_rules_version` | The versions of the workspace's chart of accounts and caption rules the mapping was made under, the `version` and `rules_version` that `GET /api/chart-of-accounts` returns.             |
| `stale`                                         | True when the chart of accounts, the caption rules, the export map or the mapping logic changed after this mapping was made.                                                             |

Every figure is a decimal string with exactly two decimal places, and every
`values` array, every measure and every entry of `foot` run in the order of
`payload.periods`, so position `i` of each belongs to period `i`. See the
[value conventions](/api/conventions).

## When a mapping is ready

A mapping is prepared after the statement's extraction, so it follows
`extraction.ready`. Three signals say when it is ready:

* **The event.** `extraction.mapped` fires once for every mapping: the first,
  and each one made again. `reason` says why: `first` for the document's first
  mapping, `extraction` when the extraction was revised, `chart` when the
  workspace's chart of accounts, caption rules or export map changed, `logic`
  when the mapping logic changed, and `correction` when none of those moved
  and the statement was corrected in the workspace. The event is a pointer:
  the document, its borrower and loan, the `mapping_id`, `status` and
  `mapping_state`, the versions and the time, never a figure. Read the mapping
  it names and let it replace the copy you hold. See
  [Webhooks](/api/webhooks) for the envelope, the fields and the delivery
  rules.
* **The retrieve.** While the statement's first mapping is being prepared, the
  retrieve answers `404` with a `Retry-After` header saying how many seconds
  to wait before reading again. A statement nobody has asked for is prepared
  by the read that finds it, so the first read of a document can answer this
  way. A `404` without `Retry-After` means no mapping is being prepared: the
  document is not a profit and loss statement, or is not one you can see.
* **The list.** `pending_document_ids` names the documents of the borrower,
  or of the loan when `loanId` is given, whose first mapping is still being
  prepared. Read the list until it is empty and every statement the borrower
  holds is in `data`.

A mapping that reads `stale: true` is still the latest one stored, served
while a fresh one is prepared: the chart of accounts, the caption rules, the
export map or the mapping logic moved after it was made. Read again to receive
the new one, or wait for the `extraction.mapped` that announces it.

```bash
curl "https://api.spreadspace.app/api/borrowers/4a2c8e1f6b3d5a7c9e0f1b2d/extractions/mappings?loanId=7d1e3b5a9c2f4e6b8d0a3c5e&limit=25" \
  -H "Authorization: Bearer ss_live_..."
```

## Mapped, unfooted, unmappable

| `status`     | `mapping_state` | What you hold                                                                                                                                                                                                                            | What to do                                                                                                                                                                      |
| ------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ready`      | `mapped`        | The statement's lines were placed on the chart of accounts and walk to its printed bottom line. `tree` carries the statement on the chart, every placed line names the node it is filed on, and the measures are the tree's own figures. | Post the tree: each account's figure per period, with its `external_code` where your export map gives one.                                                                      |
| `ready`      | `unfooted`      | The lines could not be made to walk to the printed bottom line. `tree` is null and `filed_under` is null on every line; `lines` and `measures` state the statement as printed, and `foot` says by how much each period misses.           | Hold the statement for review, or post the printed measures. A statement corrected in the workspace is mapped again, and `extraction.mapped` says so with `reason: correction`. |
| `unmappable` | none            | The statement could not be read as a profit and loss statement at all. `error_code` is `unmappable` and `payload` is null.                                                                                                               | Nothing to post. The document's own [profit loss statement read](/api/api-reference/profit-loss-statements/get-profit-loss-statement) still returns its extraction.             |

## The arithmetic

The tree is a contract you can recompute from the body, to the cent.

* Every figure on a section or an account is positive in the direction of the
  node's `effect`, `adds` or `subtracts`: a positive figure on a cost is a
  cost, a negative one is a credit against it, and a gain or loss on
  dispositions is positive for a gain.
* A node's figure is the sum of the nodes under it, measures aside, each added
  when its `effect` is the node's own and subtracted otherwise, plus the
  `filed_values` of the lines placed on the node itself, plus its
  `unitemized`. A null counts as no figure.
* `unitemized` is the part of a node's figure that no printed line accounts
  for: the statement prints a total and no detail for part of it, or the
  printed figures were rounded. It is `0.00` where the lines account for all
  of it.
* Gross profit is revenue less cost of goods sold. Operating income is gross
  profit less operating expenses. Income before income taxes is operating
  income plus other income, less other expenses, less interest, plus gains on
  dispositions. The net income row is the bottom line the statement prints.
* The sections at the top of the tree, measures aside, each counted by its
  `effect`, sum to `foot.line_sum`, and `foot.residual` is
  `printed_bottom_line` less `line_sum`. On a mapped statement the residual is
  within `foot.tolerance`, what the footing allows for the rounding of printed
  figures: a dollar, plus half a dollar for each printed figure the walk
  counts below the operating income line. A residual of `3.00` beside a
  tolerance of `8.50` is a statement printed in whole dollars, not a defect.
* A line's `values` are the figures as the statement prints them, in the
  statement's own sign presentation. Its `filed_values` are what the tree
  counts, positive in the direction of the node's `effect`.

### A worked example

A statement for the year ended December 31, 2025 prints ten lines, in whole
dollars:

| Printed line         | Figure     | Placed on                          |
| -------------------- | ---------- | ---------------------------------- |
| Consulting fees      | 418,600.00 | `pl.revenue.sales`                 |
| Total income         | 418,600.00 | none, a total                      |
| Rent                 | 72,000.00  | `pl.opex.rent`                     |
| Salaries             | 196,300.00 | `pl.opex.labor.salaries`           |
| Insurance            | 14,900.00  | `pl.opex.insurance`                |
| Insurance refund     | (1,200.00) | `pl.opex.insurance`, at `-1200.00` |
| Total expenses       | 284,000.00 | none, a total                      |
| Net operating income | 134,600.00 | none, a total                      |
| Interest expense     | 9,800.00   | `pl.interest`                      |
| Net income           | 124,801.00 | none, the bottom line              |

Its tree, showing the three sections that carry a figure (the other five are
listed with null figures), the two measure rows, and the accounts the lines
landed on:

| Node                       | `key`                    | `effect`    | `values`    | `unitemized` |
| -------------------------- | ------------------------ | ----------- | ----------- | ------------ |
| Net Revenue                | `pl.revenue`             | `adds`      | `418600.00` | `0.00`       |
|     Sales                  | `pl.revenue.sales`       | `adds`      | `418600.00` | `0.00`       |
| Gross profit               | `m:gross_profit`         | null        | `418600.00` | null         |
| Operating expenses         | `pl.opex`                | `subtracts` | `284000.00` | `2000.00`    |
|     Labor cost             | `pl.opex.labor`          | `subtracts` | `196300.00` | `0.00`       |
|         Salaries and wages | `pl.opex.labor.salaries` | `subtracts` | `196300.00` | `0.00`       |
|     Rent                   | `pl.opex.rent`           | `subtracts` | `72000.00`  | `0.00`       |
|     Insurance              | `pl.opex.insurance`      | `subtracts` | `13700.00`  | `0.00`       |
| Interest expense           | `pl.interest`            | `subtracts` | `9800.00`   | `0.00`       |
| Net income                 | `m:net_income`           | null        | `124801.00` | null         |

Read it with the rules above:

* `pl.opex.insurance` holds two printed lines. Insurance is filed at
  `14900.00`. Insurance refund is printed as `(1,200.00)`, a credit against
  the cost, so its `values` entry is `-1200.00` and so is its `filed_values`
  entry: a negative figure on a `subtracts` node. The account's figure is
  `14900.00 + (-1200.00) = 13700.00`, and its `unitemized` is `0.00`.
* `pl.opex` holds no line of its own. Its accounts sum to
  `196300.00 + 72000.00 + 13700.00 = 282000.00`, but the statement prints
  Total expenses of `284000.00` and gives no line for the other `2000.00`, so
  the section's `unitemized` is `2000.00` and its figure is `284000.00`, the
  total as printed.
* Gross profit is revenue less cost of goods sold. This statement prints no
  cost of goods sold, so `m:gross_profit` is `418600.00`.
* Operating income is `418600.00 - 284000.00 = 134600.00`, the Net operating
  income the statement prints. Income before income taxes is
  `134600.00 - 9800.00 = 124800.00`: no other income, no other expenses, no
  gains on dispositions.
* `m:net_income` is `124801.00`, the bottom line the statement prints.

The two lines placed on the insurance account, as `lines` carries them:

```json
[
  {
    "key": "pl:insurance",
    "label": "Insurance",
    "depth": 0,
    "is_total": false,
    "values": ["14900.00"],
    "sources": [{ "kind": "printed", "document_id": "8e2f5a7c1b4d4c6e9a0b3d5f", "address": "lines.4.0", "pages": [1] }],
    "filed_under": "pl.opex.insurance",
    "filed_values": ["14900.00"]
  },
  {
    "key": "pl:insurance refund",
    "label": "Insurance refund",
    "depth": 0,
    "is_total": false,
    "values": ["-1200.00"],
    "sources": [{ "kind": "printed", "document_id": "8e2f5a7c1b4d4c6e9a0b3d5f", "address": "lines.5.0", "pages": [1] }],
    "filed_under": "pl.opex.insurance",
    "filed_values": ["-1200.00"]
  }
]
```

The foot for the period:

```json
{
  "printed_bottom_line": "124801.00",
  "printed_bottom_line_source": {
    "kind": "printed",
    "document_id": "8e2f5a7c1b4d4c6e9a0b3d5f",
    "address": "lines.9.0",
    "pages": [1]
  },
  "line_sum": "124800.00",
  "residual": "1.00",
  "tolerance": "1.50"
}
```

`line_sum` is the three sections by their effect:
`418600.00 - 284000.00 - 9800.00 = 124800.00`. The statement prints
`124801.00`, so the `residual` is `1.00`. The `tolerance` is `1.50`: a dollar,
plus half a dollar for the one printed figure the walk counts below the
operating income line, the interest expense. The residual is within it, so the
statement is `mapped`: it was printed in whole dollars, and a dollar of
rounding sits between its lines and its bottom line. `measures` state the
tree's own figures: `revenue` `418600.00`, `gross_profit` `418600.00`,
`operating_expenses` `284000.00`, `operating_income` `134600.00`,
`pretax_income` `124800.00`, `net_income` `124801.00`.

## Sources

Every figure states where it comes from. A source on a tree node, on a printed
line or on the foot carries:

* `kind`: `printed` when the document prints the figure, `derived` when the
  figure is computed from figures the document prints, such as a section
  total the statement does not print or a measure.
* `document_id`: the document the figure comes from.
* `address`: a printed figure's place in the document's report data, for
  example `lines.4.0` for the figure of `lines[4]` in the first period column
  of the
  [profit loss statement read](/api/api-reference/profit-loss-statements/get-profit-loss-statement),
  so a mapped figure joins back to the printed line it came from. Null on a
  derived figure.
* `pages`: the pages the figure is printed on, numbered from 1. A derived
  figure lists the pages of the figures it is computed from.

A node's `sources` and a line's `sources` run in the order of `periods`, one
entry per figure, null where the figure is null.

## Marks and unmapped lines

Three lists on a tree node name its printed lines:

* `member_keys`: every printed line the node holds, those on the nodes under
  it included; `direct_member_keys`: the lines placed on the node itself. Each
  is the `key` of a member of `lines`.
* `flagged_keys`: lines marked for a reader's attention: an expense line whose
  caption names no expense (uncategorized, suspense), names something that is
  not an expense (an owner's draw, a loan payment, an asset purchase), or is a
  bare catch-all such as other or miscellaneous. A marked line stays where it
  is placed; the mark says a person should look.
* `reclassified_keys`: always empty. The key stays so the shape of a node is unchanged.

`unmapped_lines` lists the printed lines that carry a figure and that no
account received, each with the `section` it prints in (`revenue`, `cogs`,
`other_income`, `opex`, `other_expenses`, `interest`, `dispositions` or
`taxes`, or null when the line prints outside every section). On a mapped
statement such a line is counted on its section's own row, so the tree still
foots; `suggested_account_id` is reserved and null. On an unfooted statement no
line is placed, and `unmapped_lines` names every line that would have been.

`unmapped_export_account_ids` lists the account ids the workspace's export map
gives a code that this mapping's tree does not list, so a map entry that names
an account outside the profit and loss chart is visible from the mapping
itself.

## The chart of accounts

The tree is the workspace's chart of accounts with this statement's figures.
`GET /api/chart-of-accounts` returns the chart document the workspace saved,
its `version`, the `rules_version` of its caption rules and its
`active_export_system`. Until the workspace saves a chart, `chart` is null,
`version` is 0 and the platform's accounts below apply. A workspace's own
arrangement keeps every platform account it leaves out in force, and the tree
of every mapping lists the accounts in force with their ids, so the tree is
the authority on which accounts exist for that statement. The read needs the
`chart_of_accounts:read`, `chart_of_accounts:write` or `extractions:read`
scope.

```bash
curl https://api.spreadspace.app/api/chart-of-accounts \
  -H "Authorization: Bearer ss_live_..."
```

A section that has no accounts of its own holds its lines on the section's
row, whose key is `pl.` followed by the section (`pl.interest`) and whose
`account_id` is null.

### The platform's accounts

The account ids an export map keys on, section by section, in tree order.
Sub-accounts are indented under their parent.

### Net Revenue

The `revenue` section, key `pl.revenue`.

| Account id                      | Label                  |
| ------------------------------- | ---------------------- |
| `pl.revenue.sales`              | Sales                  |
| `pl.revenue.returns_allowances` | Returns and allowances |
| `pl.revenue.unclassified`       | Unclassified           |
| `pl.revenue.other`              | Other                  |

### COGS

The `cogs` section, key `pl.cogs`.

| Account id             | Label        |
| ---------------------- | ------------ |
| `pl.cogs.purchases`    | Purchases    |
| `pl.cogs.labor`        | Labor (COGS) |
| `pl.cogs.depreciation` | Depreciation |
| `pl.cogs.inventory`    | Inventory    |
| `pl.cogs.other_costs`  | Other costs  |

### Other income

The `other_income` section, key `pl.other_income`.

No accounts under this section.

### Operating expenses

The `opex` section, key `pl.opex`.

| Account id                     | Label                                |
| ------------------------------ | ------------------------------------ |
| `pl.opex.advertising`          | Advertising                          |
| `pl.opex.commissions`          | Commissions                          |
| `pl.opex.officer`              | Officer compensation                 |
| `pl.opex.labor`                | Labor cost                           |
| `pl.opex.labor.salaries`       |     Salaries and wages               |
| `pl.opex.labor.payroll_taxes`  |     Payroll taxes                    |
| `pl.opex.labor.pension`        |     Pension and profit-sharing plans |
| `pl.opex.labor.benefits`       |     Employee benefits                |
| `pl.opex.labor.workers_comp`   |     Workers compensation             |
| `pl.opex.rent`                 | Rent                                 |
| `pl.opex.professional`         | Professional fees and services       |
| `pl.opex.research_development` | Research and development             |
| `pl.opex.da`                   | Depreciation & amortization          |
| `pl.opex.depreciation`         |     Depreciation                     |
| `pl.opex.amortization`         |     Amortization                     |
| `pl.opex.taxes`                | Taxes and licenses                   |
| `pl.opex.bad_debts`            | Bad debts                            |
| `pl.opex.charitable`           | Charitable contributions             |
| `pl.opex.software`             | Software and subscriptions           |
| `pl.opex.bank_charges`         | Bank charges                         |
| `pl.opex.repairs`              | Repairs and maintenance              |
| `pl.opex.utilities`            | Utilities                            |
| `pl.opex.insurance`            | Insurance                            |
| `pl.opex.administrative`       | Administrative                       |
| `pl.opex.unclassified`         | Unclassified                         |
| `pl.opex.ga`                   | Other G\&A                           |
| `pl.opex.ga.travel`            |     Travel and entertainment         |
| `pl.opex.ga.meals`             |     Meals                            |
| `pl.opex.ga.office`            |     Office and supplies              |
| `pl.opex.ga.auto`              |     Auto and vehicle                 |

### Other expenses

The `other_expenses` section, key `pl.other_expenses`.

No accounts under this section.

### Interest expense

The `interest` section, key `pl.interest`.

No accounts under this section.

### Gain (loss) on dispositions

The `dispositions` section, key `pl.dispositions`.

No accounts under this section.

### Income taxes

The `taxes` section, key `pl.taxes`.

No accounts under this section.

## The export map

An export map gives each chart account the code, and the name, it has in your
own system, so a mapped statement arrives with your codes on it. A workspace
holds one map per export system and one active system.

| Operation                               | What it does                                                                                                                                                                                                                                                                      |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/chart-of-accounts/export-map` | The active map: `system`, and `entries` in order of account id, each an `account_id`, an `external_code` and an `external_label`. Until a map is saved, `system` is null and `entries` is empty. Needs `chart_of_accounts:read`, `chart_of_accounts:write` or `extractions:read`. |
| `PUT /api/chart-of-accounts/export-map` | Replaces one system's whole map and makes that system the active one. Needs `chart_of_accounts:write`.                                                                                                                                                                            |

```bash
curl -X PUT https://api.spreadspace.app/api/chart-of-accounts/export-map \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8c1f2e3d-4a5b-4c6d-9e8f-0a1b2c3d4e5f" \
  -d '{"system": "ledger_one", "entries": [{"account_id": "pl.revenue.sales", "external_code": "4000", "external_label": null}, {"account_id": "pl.opex.rent", "external_code": "6100", "external_label": "Rent expense"}]}'
```

The body is the whole map. `system` is the export system's name: a lowercase
letter followed by up to 31 lowercase letters, digits, underscores or hyphens.
`entries` holds at most 1,000 entries, each naming an account once: an
`account_id` from the tree (`pl.opex.rent`, or `t:` followed by up to 48
lowercase letters, digits and hyphens for an account the workspace added), an
`external_code` of 1 to 64 letters, digits, dots, underscores, colons or
hyphens, and an `external_label` of 200 characters or fewer, or null. An
account left out of the body leaves the map. The response is the map as saved;
a concurrent save of the same map answers `409`.

After a save, every mapping read answers `stale: true` until that mapping has
been prepared again with the new codes, and `extraction.mapped` announces it
with `reason: chart`. A mapping then carries the system in
`payload.export_system`, each account's code and name in `external_code` and
`external_label` on its tree node (null where the map gives none), and, in
`unmapped_export_account_ids`, the account ids the map names that the tree
does not list.

## Scopes and the plan

| Operation                              | Scope                                                                     | Plan                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| The two mapping reads                  | `extractions:read`                                                        | Statement mapping. Other plans get `403` with `statement_mapping_not_in_plan`. |
| The chart read and the export map read | `chart_of_accounts:read`, `chart_of_accounts:write` or `extractions:read` | Spreads. Other plans get `403` with `spreads_not_in_plan`.                     |
| The export map save                    | `chart_of_accounts:write`                                                 | Spreads. Other plans get `403` with `spreads_not_in_plan`.                     |

The `extraction.mapped` event is sent to a workspace whose plan includes
statement mapping. A document you cannot see answers `404` before any plan
signal. Every reference page states its scope and its rate limit lane; see
[Authentication](/api/authentication) for scopes and [Errors](/api/errors) for
the envelope.