Statement sets
Reconcile extracted rows against the printed balances across a whole set of statements for one account, ready for bookkeeping catch-up.
A statement set takes 1-24 already-processed bank_statement documents (uploaded and extracted via POST /v1/documents as usual, each in a terminal status) and checks them as one account's history: does every month have a statement, do balances carry forward correctly, and do any two statements duplicate the same transactions. Nothing is re-extracted and nothing is re-billed: the report is computed live from each document's already-stored result.
If our reading missed an amount, the report lists AMOUNT_UNRESOLVED and exports return 400 EXPORT_AMOUNT_UNRESOLVABLE. The message names the row to correct. Unreadable amounts never export as zero.
Review statement coverage before cleanup
Compare the supplied statements with the account and period your firm agreed to review. A statement set helps you inspect the documents already provided. Keep the agreed scope beside it so you can also spot records that have never been uploaded.
Check the edges of the period
Read the first statement’s opening date and the last statement’s closing date. Compare both with the dates you need. A set containing April through December can have continuous coverage within that range while January through March still need collecting. Ask the client about accounts opened, closed or used for only part of the year when those dates need explaining.
Confirm the account behind the files
Review the institution, printed account identifier, currency and statement dates. Resolve conflicting account information before combining files. When a masked identifier leaves the account uncertain, check the original records and ask the client to confirm which account supplied them.
Give each finding a next action
- For a gap between supplied statements, request the statement covering the uncovered dates. Equal balances on either side still leave the intervening activity unverified.
- For a break between consecutive statement periods, compare the earlier statement’s printed closing balance and the next statement’s printed opening balance with the corresponding balances in the set report. Identify the source of the difference before changing accounting records.
- For an overlap or possible duplicate, compare the source documents and the affected transactions. Two payments can share a date and amount, so review the activity before treating one as a repeat.
- For missing period information or unreadable values, compare the extraction with the original statement and obtain a clearer copy where needed. Missing statement periods exclude that statement from chain, gap and overlap analysis. Missing opening or closing balances leave the corresponding boundary comparison unverified. Unresolved transaction amounts or dates are reported separately and block the combined export.
Keep a dated coverage note
In your firm’s workpaper, record the account, requested date range, supplied date range, unresolved finding and next action. For example: “January through December requested; February statement outstanding; client to obtain the original PDF.” Update the note when a replacement arrives and review the set again.
The firm then uses the source records in its own ledger reconciliation and decides whether the evidence supports the agreed work. Keep source-data issues separate from business-purpose questions so the client knows whether to supply a document, explain a transaction or do both.
Continue to merchant categorization for purpose and category review. When preparing a year-level handoff, check the account coverage in the tax-report documentation as well.
Does a continuous statement set show that the whole requested year is present?
Compare the first and last supplied dates with the requested period. Continuity between uploaded files does not establish that earlier or later files were supplied. Check each relevant account separately.
What if the balances match across a missing statement period?
Request the missing statement. The balance could return to the same amount after several transactions. Keep that period open in the firm’s coverage note until its source activity has been reviewed.
Create a set
The response is the new set's id together with its computed report in one call, so there is no separate fetch needed right after creating it.
POST /v1/statement-sets HTTP/1.1
Authorization: Bearer sk_live_...
Content-Type: application/json
{
"document_ids": ["doc_january", "doc_march"]
}{
"id": "sts_9f2a1c",
"name": null,
"document_ids": ["doc_january", "doc_march"],
"created_at": "2026-09-27T21:04:11.000Z",
"updated_at": "2026-09-27T21:04:11.000Z",
"statements": [
{ "document_id": "doc_january", "filename": "January.pdf", "period_start": "01/01/2026",
"period_end": "01/31/2026", "opening_balance": 1000, "closing_balance": 1500,
"account_number": "****1234", "currency": "USD", "failed_checks": [] },
{ "document_id": "doc_march", "filename": "March.pdf", "period_start": "03/01/2026",
"period_end": "03/31/2026", "opening_balance": 1500, "closing_balance": 1800,
"account_number": "****1234", "currency": "USD", "failed_checks": [] }
],
"chain": [
{ "from_document_id": "doc_january", "to_document_id": "doc_march", "ok": false, "reasons": ["GAP"], "balance_verified": false }
],
"findings": [
{ "type": "GAP", "message": "No statement covers February 1 to February 28, 2026.",
"document_ids": ["doc_january", "doc_march"] }
]
}GET /v1/statement-sets/{id} recomputes and returns the same shape later (e.g. to reload the set's page). Deleting a document removes it from every set it belonged to; the report simply reflects however many documents remain.
Findings
GAP: A missing period between two consecutive statements (no statement covers those dates). Computed over the UNION of every statement’s coverage, so a statement fully CONTAINED inside a broader one is correctly treated as already covered, never as a false gap. When the gap is a single, clean calendar month AND both surrounding statements' balances are known, the message quantifies the missing month's own net activity instead (e.g. "$100.00 more went out than came in during February 2026, which has no statement. Upload February 2026's statement to see those transactions.") rather than the plain "No statement covers..." text, never worded as an error, since a missing statement already fully explains the difference.BREAK: A genuine back-to-back statement boundary's closing balance does not match the next one's opening balance; shows both numbers and the difference. Never compared across a gap, or between a statement and one merely nested/overlapping inside it (see StatementSetChainLink.balance_verified).OVERLAP: Two statements whose account numbers don't CONTRADICT each other have periods that overlap, with a count of exact-match transactions (same date, amount, normalized description) that appear in both. The combined export keeps each once. Statements that overlap but show two different, confirmed account numbers are not compared here at all (see MIXED_ACCOUNTS). Whether a match actually counts as proof additionally requires BOTH the account number and the currency to be explicitly established on both statements and equal. Unresolved identity on either side (even when both sides are equally unresolved) doesn't skip the pair, but nothing in it can be merged; the outcome is recorded in POSSIBLE_DUPLICATE.POSSIBLE_DUPLICATE: Within an OVERLAP, a transaction pair that looks like a duplicate but isn't proven to be one: a same-date/same-amount pair with a DIFFERENT description (a pending charge posting later, or OCR noise); an exact date/amount/description match with a running balance on only one side, or unequal running balances on the two sides; or an exact match whose two statements were never BOTH confirmed to have the same account number AND the same currency. When neither transaction has a running balance, an exact date, amount and normalized-description match can be merged if the account and currency identity requirements are also met. Currency: an explicit ISO code, never a bare symbol. Account: the two identifiers, normalized (spaces/dashes stripped, mask characters kept), must be STRING-IDENTICAL: both full numbers equal, or both masked with the identical visible digits and mask shape; a value with no digits at all ("****", "N/A", "xxxx") counts as unknown; masked vs full ("****5678" vs "12345678") is never proof of the same account. Never auto-merged; kept as separate lines for a bookkeeper to check.DUPLICATE_UPLOAD: Two documents in the set are byte-for-byte the same file AND their extractions agree on everything: transactions (compared as a MULTISET at full extracted precision, never rounded to the cent, 995.001 and 995.004 are a conflict, not a match, ignoring row order), period (compared as real calendar dates, so "03/01/2026" and "2026-03-01" count as equal), opening/closing balance, account number, and currency (an accidental double upload). The duplicate contributes no lines to the combined export.DUPLICATE_UPLOAD_CONFLICT: Two documents are the same uploaded file, but their extractions DISAGREE on any of transactions, period, opening/closing balance, account number, or currency, after normalizing dates and ignoring row order (e.g. a re-processed pass recovered an extra row, or read a different closing balance. A reordered or differently-formatted-but-equal extraction is NOT a conflict). Never auto-resolved either way, in either document order; blocks the combined export (400 EXPORT_DUPLICATE_CONFLICT).MIXED_ACCOUNTS: The statements don't all show the same account number, checked pairwise across every statement (not just against one reference, which a masked number could otherwise transitively bridge between two genuinely different accounts). A masked and an unmasked view of the same account are compared by their printed digits, never falsely flagged. Blocks the combined export (400 EXPORT_MIXED_ACCOUNTS); split the set by account.CURRENCY_MISMATCH: The statements use different currencies, resolved ONLY from an explicit ISO 4217 code printed as text (USD, EUR, CAD, CNY, ...), never from a bare symbol alone ($, ¥, £, kr, ...), since each of those is shared by multiple real currencies: "CAD $" resolves to CAD, "CNY ¥" resolves to CNY, and a bare "$" or "¥" resolves to nothing rather than silently defaulting to USD/JPY (see UNIDENTIFIED_MEMBER). Blocks the combined export (400 EXPORT_MIXED_CURRENCY); split the set by currency.UNIDENTIFIED_MEMBER: A statement has no USABLE account number (a placeholder with no digits at all, "****", "N/A", "xxxx", counts the same as a genuinely missing field), or no explicitly-coded currency, while another statement in the set has one. Not treated as a mismatch (absence isn't evidence of one), but never treated as proof it belongs to the group's identity either. Blocks ofx specifically (400 EXPORT_UNIDENTIFIED_MEMBER, since only ofx declares one ACCTID/CURDEF for the whole file); csv/xlsx/qbo_csv/xero_csv have no such column to misattribute, so they still proceed with this noted.ACCOUNT_UNPROVEN: Two statements' account numbers don't CONTRADICT each other (a masked and an unmasked view can plausibly be the same account, see MIXED_ACCOUNTS), but nothing confirms they're the SAME account either: a full "12345678" alongside a masked "****5678" is a guess either way. Both statements DO have a usable account number (otherwise see UNIDENTIFIED_MEMBER instead), so this is a distinct, narrower gap. Blocks ofx specifically (400 EXPORT_UNIDENTIFIED_MEMBER, reused since the guarantee is the same: never let one member's account silently stand in for another's that was never actually confirmed); csv/xlsx/qbo_csv/xero_csv have no such column to misattribute, so they still proceed with this noted.FAILED_CHECKS: One statement's own verification checks (e.g. opening + transactions = closing) did not all pass.MISSING_PERIOD: A statement has no period_start/period_end, so it is included in the set but excluded from the chain and gap/overlap analysis.DATE_UNRESOLVED: A statement's own transaction dates could not be resolved. Just like a single statement's own export, this blocks the combined export (400 EXPORT_DATE_UNRESOLVABLE) rather than silently exporting a truncated file.RESULT_UNAVAILABLE: A document is still listed in the set, but its stored extraction result could not be loaded. It's excluded from this report, and blocks the combined export (400 RESULT_UNAVAILABLE) until removed or re-uploaded.
A single statement supports merchant review on Free. Confirm its merchants, then see My year. Cross-statement checks apply when the set contains multiple statements.
Combined export
This is a categorized export (an "Account" column once categorization has run), part of the bookkeeping product: locked on Free, unlocked during a Catch-Up purchase’s 12-month access term, or a live Autopilot/Business subscription (402 EXPORT_LOCKED otherwise). The report above (GET /v1/statement-sets/{id}, gap detection and reconciliation) is never locked this way. It stays priced purely per page, like the rest of document extraction.
GET /v1/statement-sets/{id}/export?format=csv|xlsx|qbo_csv|xero_csv|ofx produces ONE file covering every statement in the set: transactions merged and date-ordered, with an overlap's duplicate transactions included once (never dropping two genuinely distinct same-day, same-amount lines that both appear in a single statement). Every kept line stays traceable to its own source document. ofx FITIDs follow the exact same identity rules as a single statement's own OFX export, computed per source document, so two different lines can never collide (asserted before the file is ever returned). Its LEDGERBAL is the chronologically latest statement's own closing balance, but only among statements with a resolvable period_end to date that balance by, with none, which of several undated balances is "the latest" can't be established from upload order alone, so the export refuses (400 EXPORT_MISSING_BALANCE) rather than let reversing the input order silently change the exported balance. DTSTART/DTEND always cover every emitted transaction's own date, not just the nominal statement periods.
This endpoint never silently drops or merges a real transaction: it refuses the whole export with a specific 400 error whenever that would otherwise happen: EXPORT_MIXED_ACCOUNTS, EXPORT_MIXED_CURRENCY, EXPORT_DATE_UNRESOLVABLE, EXPORT_DUPLICATE_CONFLICT, EXPORT_UNIDENTIFIED_MEMBER (ofx only), RESULT_UNAVAILABLE, or (a defensive last resort)EXPORT_FITID_COLLISION. See the finding it corresponds to above for why.
Downloading these exports counts as delivery under the 30-day money-back guarantee. While an automatic refund is pending, paid downloads in that business pause until it completes. Paid download attempts return 409 EXPORT_LOCKED during this pause. Failed or canceled refunds release the pause. Support reviews requests made after delivery under the same guarantee.