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

# Exports

An export takes a set of documents on one loan and bundles their extracted
line items into a single file. It runs in the background, so the request that
starts it answers immediately with an export to poll rather than with the
file.

Three steps, in the Extractions group of the [API reference](/api). Creating
and cancelling an export need the `extractions:export` scope; reading and
downloading need `extractions:read`.

| Step           | Operation                                           |
| -------------- | --------------------------------------------------- |
| 1. Start it    | `POST /api/extractions/exports`                     |
| 2. Poll it     | `GET /api/extractions/exports/{export_id}`          |
| 3. Download it | `GET /api/extractions/exports/{export_id}/download` |

**Create** answers `202` with the export in `queued`. **Poll** the same export
until `status` is terminal. When it reaches `succeeded`, the response carries
the bundle and a download link.

```bash
curl -X POST https://api.spreadspace.app/api/extractions/exports \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f7c31c9-3a06-4a5f-9f6b-8d34ef9f2b41" \
  -d '{"borrower_id": "4a2c8e1f6b3d5a7c9e0f1b2d", "loan_id": "7d1e3b5a9c2f4e6b8d0a3c5e", "document_ids": ["1f2e3d4c5b6a79081a2b3c4d", "9a8b7c6d5e4f30215d6e7f80"], "format": "xlsx"}'
```

```bash
curl https://api.spreadspace.app/api/extractions/exports/9f8b7c6d5e4a3b2c1d0e9f8a \
  -H "Authorization: Bearer ss_live_..."
```

Poll every few seconds. `progress` carries `completed` and `total`, counting
documents processed against documents requested, so a long export can be shown
as it moves. `GET /api/extractions/exports` lists the exports you started,
newest first, filtered with `status` and paged with `cursor`.

## Statuses

| Status       | What it is                                                                                                        |
| ------------ | ----------------------------------------------------------------------------------------------------------------- |
| `queued`     | Accepted and waiting for a worker. Nothing has been read yet.                                                     |
| `running`    | A worker is reading the documents and building the bundle.                                                        |
| `cancelling` | A cancel landed mid-bundle. The worker stops at its next document boundary and the export then reads `cancelled`. |
| `succeeded`  | The bundle is built and can be downloaded.                                                                        |
| `failed`     | The export stopped without a bundle. `error_code` and `error_message` say why.                                    |
| `cancelled`  | Stopped on request. No bundle.                                                                                    |

`succeeded`, `failed` and `cancelled` are terminal. `cancelling` is not: keep
polling and it settles on `cancelled`.

`error_code` on a failed export is one of `no_documents_resolved` (none of the
ids named a document on that borrower and loan), `export_ceiling_exceeded`
(more documents than one export may carry), `stale_running_exceeded`
(the export stopped making progress and was abandoned), or
`internal_error`.

## Documents and counts

`borrower_id`, `loan_id` and `document_ids` are all required. `document_ids`
names between one and one thousand documents, and a request missing any of the
three is rejected with `400`. Ids are resolved inside the borrower and loan you
named, and an id that does not resolve there is skipped rather than failing the
export.

| Field                       | What it says                                                                                     |
| --------------------------- | ------------------------------------------------------------------------------------------------ |
| `document_counts.requested` | How many ids you sent.                                                                           |
| `document_counts.exported`  | How many of them are in the bundle.                                                              |
| `document_counts.skipped`   | How many did not resolve on that borrower and loan.                                              |
| `skipped_document_ids`      | The ids counted in `skipped`. Empty until the export resolves them.                              |
| `bundle`                    | `name`, `size_bytes` and `line_item_count` for the built file. `null` until the export succeeds. |

Read `skipped_document_ids` before you trust a bundle: an export of ten
documents that skipped four still succeeds, and the counts are the only place
that says so.

## Formats

`format` is `xlsx`, `csv` or `json`, and defaults to `xlsx`. **`xlsx` covers
bank statements only**, so pass it only when every document in the export is a
bank statement; use `csv` or `json` for a mixed set. The values inside a
bundle follow the house [value conventions](/api/conventions): money is a
string with exactly two decimals, counts are integers, and timestamps are
RFC 3339 UTC.

## Downloading

A succeeded export carries `download_url` and `download_expires_at`. **The
link is minted fresh on every read**, and it is good for minutes rather than
hours. Do not store it: read the export again when you need another one. The
link and its expiry roll over at each five-minute signing boundary, so two
reads inside the same window return the same link and the next window
returns a new one. A link that has run out is not an error to retry against,
it is a link to replace.

`links.download` points at
`GET /api/extractions/exports/{export_id}/download`, an authenticated door to
the same file. It answers `302` with a freshly minted link in `Location`, so a
client that follows redirects can pull the bundle in one call. It answers
`404` until the export has succeeded, and again once the export has expired.

## Cancelling

`POST /api/extractions/exports/{export_id}/cancel` stops an export that has not
finished.

* A `queued` export is cancelled at once and reads `cancelled`.
* A `running` export moves to `cancelling`. The worker acknowledges at its
  next document boundary and the export then reads `cancelled`. `links.cancel`
  is `null` from that point, and a second cancel answers `200` with the export
  unchanged.
* An export that already `succeeded` or `failed` answers `409`.
* An export already `cancelled` answers `200`, unchanged.

## Retention

`expires_at` is seven days after the export was created. At that time the
bundle and the export record are both deleted, and every operation on that id
answers `404` from then on. Pull what you need inside the window; there is no
way to revive an expired export, so start a new one.

## Idempotency

Send an `Idempotency-Key` on create and a retry within 24 hours replays the
original `202` with `Idempotency-Replay: true`; after that window the same key
returns the existing export with `200` for as long as the export exists.
Reusing the key with a different body is a `409`. See
[Idempotency](/api/idempotency) for the full rules.

## Rate limits

Creating an export rides the export lane at **120 per minute**. Get, list and
cancel ride the general lane. See [Rate limits](/api/rate-limits).

## Test mode

A test-mode key can create exports. It exports the sandbox workspace's own
documents, the response shape is the same as in live, and the events it
generates carry `livemode: false`.

## Webhooks

Three events report the end of an export: `export.succeeded`,
`export.failed` and `export.cancelled`. Each carries identifiers, the
`format`, and a timestamp saying when the export reached that state;
`export.succeeded` also carries `document_count`, and `export.failed` carries
`reason`, which is the export's `error_code`. **No event carries a download
link.** Read the export when one arrives, and use the `download_url` on that
response. See [Webhooks](/api/webhooks) for the envelope, signing and
delivery rules.