S2SStmt2Sheet

Formats · accuracy · privacy · review

Bank statement converter FAQ

What the converter accepts, what comes out, and where you still have to check. The rules below are read from the code that runs the conversion.

Supported formats: what the code accepts

This is the input contract enforced by app.js, worker.js and converter.js — not a marketing list.

InputRule in the codeWhere
File typePDF only. Accepted when the name ends in .pdf (any case) or the browser-declared type is application/pdf.app.js, worker.js
Not acceptedJPG, PNG, HEIC, WEBP, DOCX, XLSX, CSV and ZIP files. A PDF that prompts for a password is not accepted either.upload check, parser
Size per file15 MB (15,728,640 bytes). The page blocks a larger file before upload.both
Size per request30 MB across all files in one batch.worker.js
Files per batchSeveral PDFs in one request; rows are concatenated in upload order, not sorted by date.both
Text layerAt least 40 non-whitespace characters. Below that, the PDF is treated as image-only and routed to OCR.converter.js
PagesEvery page is parsed. There is no page range or page-selection parameter.converter.js
Rows per export10,000 maximum on /api/export.worker.js
LanguageHeader labels are English and matched as whole cells. The OCR call passes no language setting in the code.converter.js

What each export contains

ExportFileContentsDetails that matter
XLSX.xlsx, one sheet named TransactionsDate, Description, Debit, Credit, Amount, BalancePlain values, no formulas or formatting. Debit and credit are positive in their own columns; Amount is signed.
CSV.csv, UTF-8, CRLF, header rowThe same six columnsFields containing commas, quotes or newlines are quoted. No BOM.
QBO.qbo, OFX 1.0.2One bank statement with a transaction per row and a ledger balanceCURDEF is hardcoded USD, account type CHECKING, account ID STATEMENT. Each export generates random FITIDs, so the same file imported twice is not deduplicated.
Xero CSV.csvDate, Amount, Payee, Description, ReferenceAmount is signed. Reference is literally stmt2sheet. Payee is the first segment of the description, capped at 80 characters.
Tally XML.xmlImport envelope with one Bank voucher per rowUses Bank and Suspense ledgers and an empty company by default; dates are YYYYMMDD.
JSONAPI onlyTransactions, warnings, pages and the trial countAvailable from /api/convert?format=json.

Header labels the parser recognizes

A header line must contain a recognized date label, a recognized description label, and at least one of debit, credit or amount. Matching is case-insensitive against the whole text item, so a label with a unit or a suffix is a different label.

ColumnAccepted header text
Datedate, posting date, posted date, transaction date
Descriptiondescription, details, transaction, memo, payee
Debitdebit, debits, withdrawal, withdrawals, money out, payments
Creditcredit, credits, deposit, deposits, money in
Amountamount, transaction amount
Balancebalance, running balance

Labels such as Value Date, Amount (GBP) or Debit/Credit are not recognized. A line without a recognized header is not a dead end: a date at the start of the line is still accepted, and the first two numbers on it are read as balance and amount. That fallback is why a non-standard header can still produce rows with the amount and balance swapped.

Dates and amounts the parser reads

ValueAcceptedNot accepted, or read differently
DateMM/DD/YYYY, MM/DD/YY (becomes 20YY), MM-DD-YYYY, MM.DD.YYYY, MM/DD, YYYY-MM-DD, Aug 28, Aug 28 2026DD/MM/YYYY is read as month/day; 28 Aug 2026 and 2026/08/28 are not recognized. A date with no year takes the first 20xx number on the page, then the year from an earlier page, then the server's current year.
Amount1,234.56, -12.50, 12.50-, (12.50), $12.50, €12.50, £12.50, 12.5USD 12.50, 1.234,56 and 12.345 are rejected. 1234,56 is read as 123456, because a comma is always a thousands separator.

Which banks are covered

There is no bank allowlist and no per-bank template. The parser works from the PDF's text layer and the header labels, so a statement can convert regardless of which bank printed it, as long as the text layer is readable and the header matches. The same rule means a statement from a bank that worked before can fail after the bank changes its layout.

The repository has no tested-bank list. Its tests build synthetic statements with known rows in two layouts, one with separate debit and credit columns and one with a single signed amount column. They are regression tests for the parser, not a coverage list, and no real-bank sample set is published. No bank is claimed as verified here.

Credit card statements use the same parser. Invoices, receipts and spreadsheets are different formats and are not accepted; export CSV or QBO to move statement rows into another tool.

Accuracy: what is measured, and what is not

No accuracy percentage is published. No labeled set of real statements has been run and scored for this site, so any number would be invented. What is true from the code: for a PDF with a text layer the extraction is deterministic — the same PDF produces the same rows, which makes a bad result reproducible. It does not make the result complete: the parser can return a partial table. For a scan, the result depends on the configured OCR provider and the scan quality, and no provider is named in the code.

The parser's known failure modes are documented on troubleshooting: day-first dates, a missing or non-standard header, side-by-side tables, duplicate rows and comma decimals. The review step below is how you catch them.

The review step

The preview lists every parsed row before any download. The export buttons send those exact rows to the export endpoint; they do not re-parse the PDF. Nothing is posted to a ledger and no row is categorized.

  1. Row count. Compare with the statement's transaction count or its page footers.
  2. Dates. Check the first and last date, the year, and any date whose day is 12 or less — that is where a day-first statement is silently swapped.
  3. Signs. Refunds, chargebacks, transfers and payments are the rows most likely to land on the wrong side.
  4. Balances. The last row's balance should equal the statement's closing balance. If it does not, a row is missing or misplaced.

The preview is read-only: there is no cell editing, row deletion or reordering. To change a row, download XLSX or CSV and edit it in a spreadsheet, keeping the original PDF beside it. A preview followed by a download counts the pages once, because the download reuses the parsed rows.

Privacy, in one paragraph

The PDF is uploaded to /api/convert and parsed on a Cloudflare Worker in memory; no statement is written to storage. No AI or OCR service is called unless the deployment has an OCR provider configured, and then only for an image-only PDF. The trial cookie stores a page count and an HMAC signature, not the statement. The data-handling page shows the exact path, the third parties and the deletion timing; privacy is the policy.

Questions

What file types can I upload?

PDF only, up to 15 MB per file and 30 MB per batch. JPG, PNG, DOCX and CSV files are rejected, and a password-protected PDF fails to parse.

Which banks are supported?

There is no bank list. The parser uses the PDF's text layer and a header line with date, description and debit, credit or amount columns, rather than per-bank templates. No tested-bank list is published.

Does it work with scanned statements?

Only when the deployment has an OCR provider configured. Without one, an image-only PDF returns ocr_not_configured instead of invented rows, and /api/health reports whether OCR is on.

What accuracy can I expect?

No accuracy figure is published, because none has been measured on labeled real statements. Text PDFs extract deterministically; scans depend on the provider and scan quality. Review every row.

Are transactions categorized or matched to invoices?

No. The converter returns the statement's rows. Categorize and match them in Excel or in your accounting software after export.

Is there a review step before export?

Yes. The preview lists every parsed row before any download, and the export uses exactly those rows. The preview is read-only; to change a row, download XLSX or CSV and edit it in a spreadsheet, keeping the original PDF beside it.

How does the 5-page trial count pages?

One page is one PDF page, including a cover page. The pages across the files in a request are added to the signed cookie count; requests that return no rows do not use pages.

Is my statement stored or sent to an AI service?

It is uploaded and parsed in memory, and no copy is written to storage. There is no AI step in the code; OCR is used only when the operator configured a provider and the PDF has no text layer.

What happens with a password-protected PDF?

If the PDF prompts for a password, the parser cannot open it and returns parse_failed. Save an unlocked copy from the bank and upload that.

Is there an API?

Yes. POST /api/convert returns rows or a file, and POST /api/export re-exports rows you already parsed. Error codes are on the troubleshooting page, and an API_KEY can bypass the browser trial.