Symptom → cause → fix
Bank statement PDF troubleshooting
The converter fails in a finite set of places. Find the symptom you saw, the branch that produced it, and the check that fixes it.
Ready.
| Date | Description | Debit | Credit | Amount | Balance |
|---|
Your PDF is processed in memory and discarded after the response. We do not store statements.
Classify the failure first
- The request failed. No table appears and the status line shows a short code or the parser's own message. Use the error tables below.
- The table appeared but rows are missing or wrong. The PDF parsed; the parser made a decision you can check. Use the row-level table.
- Nothing appeared. Look at the browser's network log for the
/api/convertrequest. An upload that was blocked or dropped never reaches the parser.
When the request fails
These are the codes worker.js returns from /api/convert, with the condition that produces each one. The browser client shows the response's message when there is one, replaces trial_exhausted with a sentence of its own, and otherwise shows the raw error code — which is why the status line sometimes shows a single word.
| Code | HTTP | What triggers it | What to do |
|---|---|---|---|
pdf_required | 415 | The file name does not end in .pdf and the browser-declared MIME type is not application/pdf. | Upload the PDF itself. A JPG, PNG or DOCX is rejected here; an image renamed to .pdf gets further and then fails at parsing. |
file_too_large | 413 | One file is over 15 MB (15,728,640 bytes). A request whose declared Content-Length is over 45 MB is also rejected before parsing. | Split the document, or export only the period you need. The 15 MB limit is per file. |
batch_too_large | 413 | The files in one request add up to more than 30 MB. | Send separate batches. |
missing_file | 400 | The multipart request has no file part. | Choose the PDF again in the page. API clients must send multipart/form-data with a file field. |
invalid_multipart | 400 | The body could not be read as multipart/form-data. | Retry from the page. API clients should use a standard multipart encoder. |
unsupported_format | 400 | The format parameter is not json, csv, xlsx, qbo, xero or tally. | Use one of those six values. |
no_transactions | 422 | Every file parsed, but no line produced a usable date and amount. The response includes a warning for each empty file. | Use the row-level table. The usual causes are no text layer, an unrecognized header and a day-first date. |
ocr_not_configured | 422 | The text layer has fewer than 40 non-whitespace characters and the deployment has no OCR provider configured. | Download the bank's text PDF, or have the operator set OCR_PROVIDER_URL and OCR_API_KEY. |
parse_failed | 422 | The PDF could not be opened — encryption or damage — or the OCR provider returned an error, a network failure or an unrecognized response shape. The parser's message is included, cut to 300 characters. | Make an unlocked copy and upload that. For a provider error, retry once: the code makes one attempt per request. |
trial_exhausted | 402 | The pages parsed in this request plus the count already in the cookie exceed 5. | See the trial section below. The whole PDF is parsed before the count is checked, so a 6-page file always fails. |
method_not_allowed | 405 | A non-POST request to /api/convert. | Use POST; the page does. |
On /api/export — the request the browser makes when you click a download button — there are three more:
| Code | HTTP | What triggers it | What to do |
|---|---|---|---|
too_many_rows | 413 | More than 10,000 rows in the transactions array. | Export a smaller batch. The preview has no row cap, so a large batch can preview and then fail here. |
missing_transactions | 400 | The transactions array is missing or empty. | Run the preview again; the download buttons send the rows from it. |
invalid_json | 400 | The request body is not valid JSON. | API clients only; the page always sends JSON. |
checkout_not_configured (503, no payment link set for the plan) and not_found (404, unknown /api/ path). unsupported_format is shared by both endpoints.When the table appears but values are wrong
Nothing below is a request error: the response is HTTP 200 and contains rows. Each item follows from a rule in converter.js, so the fix is a check you can repeat, not a retry.
| Symptom | Cause in the code | What to do |
|---|---|---|
| About half the rows are missing on a page | The statement prints two transaction tables side by side. Lines are built by grouping text at the same vertical position across the full page width, and only the first date on a line is used, so the second table's rows on the same baseline are not emitted. | Upload one account per PDF. If you cannot get separate statements, split or crop the columns into separate PDFs, then compare each row count with its statement. |
| Two identical transactions became one | After parsing, rows with the same date, description, debit, credit and balance are dropped as duplicates, to suppress repeated page headers and overlapping text. Two genuinely identical rows with no running balance collide. | Compare the row count with the statement and re-add the missing row in Excel. The PDF remains the source. |
| A date's day and month are swapped | Dates are read as month/day, never day/month. 05/08/2026 becomes 8 May 2026; 28/08/2026 has month 28, is rejected, and the line is skipped unless it merges into a description. | Check every date in the preview. Prefer statements that print an unambiguous month; correct day-first dates after export. |
| The year is wrong | A date without a year takes the first 20xx number found on the page; if that page has none, it uses the year carried from an earlier page, or the server's current year. | Check the year in the preview and correct it in Excel. A printed statement period normally supplies the year. |
| Amount and balance are swapped | When no header line is recognized, the first number on a line becomes Balance and the second becomes Amount. A layout that prints amount before balance comes out reversed. | Compare the header with the accepted labels on the FAQ page. If the header is non-standard, verify amount and balance on every row. |
| Numbers landed in the wrong column | With a header, each token goes to the nearest header position by x coordinate. A header that is not aligned over its column, or right-aligned numbers under left-aligned headers, can shift values between Debit, Credit and Balance. | Check the first rows. If the columns are shifted, treat the layout as unreliable and correct the file in Excel, or use a different export from the bank. |
| Amounts are 100 times too large, or a row disappeared | Commas are always thousands separators and the decimal separator must be a dot. 1234,56 becomes 123456; 1.234,56 is rejected; USD 12.50 is rejected because only $, €, £ and bare numbers are stripped. A row without a readable amount is dropped. | Check totals against the statement. For comma-decimal statements, correct the amount column after export. |
| A description is cut short, or a continuation line is missing | A line without a date is appended to the previous row only if it contains no money token, a header has already been found, and its y coordinate is below 720 points. That threshold is fixed, not a fraction of the page height, so a continuation line near the top of an A4 page is skipped. | Check multi-line descriptions in the preview and repair them in Excel if needed. |
| Later pages contributed nothing, but the request succeeded | A page that parses zero rows adds a warning (page N: no transactions parsed) instead of failing, as long as another page or file produced rows. The browser client does not display the warnings array. | Compare the row count with the statement and its page count. Upload the missing page as its own PDF. |
| The header from page 1 is applied to page 2 | A header, once found, is carried across pages. A later page with a different layout still uses the first page's column positions. | Check the later pages. If the layout changes, split the PDF. |
| A refund or payment is on the wrong side | Debit and credit are read from their own columns. When only an amount is present, the sign decides: negative is debit, positive is credit. A statement that prints all amounts positive and marks direction in separate text gives the parser no sign to read. | Check refunds, chargebacks and transfers first; correct the sign after export. |
Scanned and image-only statements
The OCR decision is made before table parsing and it is one rule: extract the text of every page, remove whitespace, and if fewer than 40 characters remain, treat the PDF as image-only. A PDF whose only text is a watermark or a page number can land in the same branch as a pure scan.
- No OCR provider configured. The request stops with
ocr_not_configuredand the message that an OCR provider is required. Nothing is invented and nothing is stored. The deployment's/api/healthresponse reports whether a provider is configured; when it saysocr: false, every scan on that deployment ends here. - Provider configured. The whole PDF is base64-encoded into a JSON body and sent to
OCR_PROVIDER_URLwith the bearer key fromOCR_API_KEY. The provider must return either atransactionsarray or apagesarray of positioned text items. An HTTP error or any other shape becomesparse_failed; there is one attempt per request and no queue. - Quality. No accuracy figure is published because none has been measured against labeled real statements. OCR can misread a sign, join a description to the wrong amount or drop a column. Check the preview row by row, starting with the opening and closing balance.
- A photo saved as PDF is still a photo. Converting a JPG to PDF outside this tool does not create a text layer, so the same OCR branch applies. Straighten and crop the page, avoid shadows, and use the highest optical resolution your scanner offers. If the bank offers a text PDF, use that instead.
Multi-page statements, batches and the trial
- Every page is parsed. Pages are processed in order; rows are concatenated in file order and are not sorted by date. A cover page is parsed and counted like any other page.
- The trial counts pages, not statements. The count is the total number of pages in the uploaded PDFs. Five pages are included. The check runs after parsing, so a file that is too long for the trial is parsed first and then rejected with
trial_exhausted. - Failed requests do not use pages. A request that returns no rows, fails to parse or needs an unconfigured OCR provider returns before the count is checked, so the cookie is not written. A request that returns rows writes a new count.
- Preview and download count once. The download buttons send the parsed rows to
/api/export, which does not parse the PDF again and does not touch the page count. - A batch is all-or-nothing on errors, not on empty files. If one file fails to parse, the whole request returns
parse_failedand rows from the other files are discarded. If a file simply produces no rows, the request succeeds; the empty file appears in warnings, which the page does not display. Compare the row count with each statement, or readfilesandrowsfrom the API response. - There is no page selection. The converter cannot skip a cover page, convert pages 3–5 only, or re-run OCR on a single page.
Before you upload
- The file is a PDF downloaded from the bank, not a photo or a screenshot.
- You can select a transaction with the mouse in a PDF reader — that means there is a text layer.
- The PDF opens without a password prompt.
- The file is 15 MB or less and a batch is 30 MB or less.
- One account per PDF, with no second table printed beside the first.
- The header is on one line and uses a label from the list on the FAQ page.
- The dates include a year, or use the month/day order the parser expects.
- The statement is 5 pages or less for the trial, and the export will be 10,000 rows or less.
Questions
Why does the status line show a word like no_transactions?
That is the API's error field. The browser shows the response message when the API supplies one and the raw code when it does not; the error tables above list every code and its cause.
Is a partly parsed statement an error?
No. The request returns 200 with the rows it found. Pages that produced none are reported in a warnings array that the page does not display, so compare the row count with the statement.
Can I convert only some pages?
No. Every page of the PDF is parsed and counted. Split the PDF before uploading if you need a subset.
Does a failed conversion use up trial pages?
No. Requests that return no rows, fail to parse or need an unconfigured OCR provider return before the page count is checked. A request that returns rows counts every page in the upload.
Does the converter retry OCR automatically?
No. There is one OCR call per request, with no retry or queue; a provider error is returned as parse_failed.