> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.spreadspace.app/api/statement-mapping/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. > A profit and loss statement placed on the chart of accounts: the figures a loan origination system posts, recomputable from the body.