Data handling, read from the code
What happens to your statement PDF
The PDF leaves your browser, is parsed on a Cloudflare Worker in memory, and is not written to storage. No OCR or AI service sees it unless the deployment has one configured.
The short answer
The PDF leaves your browser. It is sent to /api/convert on this site's Cloudflare Worker, parsed in memory, and returned as rows or an export file. No copy is written to storage. No OCR or AI service is called unless the operator of the deployment has configured one, and then only for a PDF with no text layer.
The rest of this page separates what the code guarantees from what the deployment decides.
The path of one PDF
| Step | What happens | Where the data is |
|---|---|---|
| 1. You choose the file | app.js checks the name or MIME type and the 15 MB limit, then sends the file as multipart/form-data to /api/convert. The PDF is not read or parsed in the browser. | Browser memory |
| 2. The Worker receives it | The Worker reads the whole file into memory and enforces 15 MB per file and 30 MB per request. | Worker memory |
| 3. Text is extracted | unpdf 1.9.1, which wraps pdf.js, returns positioned text items: text, x, y, width and height. The library is bundled into the Worker and makes no network call. | Worker memory |
| 4. Rows are parsed | Items are grouped into lines by vertical position, matched against header labels, assigned to columns by x position, and emitted as rows. Exact duplicates are dropped. | Worker memory |
| 5. OCR is considered | If the extracted text is under 40 non-whitespace characters, the PDF is image-only. With a provider configured, the whole PDF is base64-encoded and sent to it; without one, the request ends with ocr_not_configured. | Worker memory, plus the OCR provider only when configured |
| 6. The response is built | JSON rows or an export file, with Cache-Control: no-store. No storage write, queue or webhook receives the statement. | Response body |
| 7. You download | Clicking XLSX, CSV, QBO, Xero or Tally sends the parsed rows — not the PDF — to /api/export, which builds the file in memory. | Browser to Worker memory |
| 8. After the response | The request's memory is released. The parsed rows stay in the page until you reload or close the tab. | — |
Does the file leave my browser?
Yes. The only conversion request is a fetch to /api/convert whose body is a FormData containing the PDF. There is no client-side parser, no service-worker cache and no local-first mode. If you need the statement to stay on your own machine, this converter is the wrong tool.
The preview does not change that: the table is built from the JSON the Worker returns. A second request happens only when you click a download button, and it carries the parsed rows rather than the PDF.
Third parties
| Who | Receives the PDF? | When | What governs it |
|---|---|---|---|
| Cloudflare | Yes, in transit and in Worker memory | Every conversion | Cloudflare hosts the site and runs the Worker. Its privacy policy covers request metadata such as IP address, URL and user agent. Checked 2026-10-30. |
| OCR provider | Yes — the full PDF as base64 inside a JSON body | Only when OCR_PROVIDER_URL and OCR_API_KEY are both set, and only for an image-only PDF | The provider is chosen by the site operator and is not named in the code. Its own terms and retention apply. The code sends one request per conversion and has no delete call. |
| Dodo | No | Only when you open a checkout link | The payment page. It never receives statement data, and this site never receives card details. |
| AI or analytics vendors | No | Never | There is no AI model or third-party analytics script in the code. app.js is the only script and it is first-party. |
What is kept, where, and for how long
| Data | Where it exists | How long | Contains the statement? |
|---|---|---|---|
| PDF bytes | Browser memory (the selected file) and Worker memory | Until the page is reloaded on your side; for the duration of the request on the server | Yes |
| Extracted rows | Worker memory during the request; afterwards in a JavaScript variable and the preview table in your browser | Until you reload or close the tab | Yes |
| Exported file | Your download folder | Until you delete it | Yes |
Trial cookie s2s_trial | Your browser, for this domain | 30 days, renewed each time a new count is written | No — page count and a signature only |
| Request metadata | Cloudflare's edge and logs | Governed by Cloudflare's policy, not by this code | No request body is logged by the Worker |
| Account, statement database, object store | Does not exist | — | — |
Deletion timing
There is no deletion schedule because there is no stored copy. When the Worker returns the response, the runtime releases that request's memory. Two things outlive the request: the page-count cookie and whatever Cloudflare records about the request. Neither contains the statement. If an OCR provider is configured, the provider holds its own copy under its own retention policy; this code sends the PDF once and never asks the provider to delete it.
The rows in your browser are gone when you reload or close the tab. A downloaded file is yours and is deleted by you.
What the trial cookie stores
| Property | Value in the code |
|---|---|
| Name | s2s_trial |
| Value | An integer page count, a dot, and a 64-character HMAC-SHA256 hex signature made with the TRIAL_SECRET Worker secret |
| Flags | HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000, plus Secure on HTTPS. HttpOnly keeps the value out of page JavaScript. |
| Written | After a conversion that returns at least one row; the count is the total pages of the uploaded PDFs. An OCR response that returns rows without a page count is counted as one page. |
| Not written | When the request fails before the page-count check (no rows, parsing failure, OCR missing), on /api/export, or when TRIAL_SECRET is unset |
| Contains | The page count only: no statement, transactions, file name, timestamp, user ID or IP address |
| Lifetime | 30 days, renewed when a new count is written; cleared when you clear site data for this domain |
| Server copy | None. The count lives only in your browser |
There is no account and no server-side usage record to look up. Clearing site data or using a different browser starts the count again; the page count is the cookie. When API_KEY is set, an API request carrying the exact Authorization: Bearer key is not counted and receives no cookie.
Deployment-dependent behavior
This repository cannot tell you which secrets the production Worker has. These are the switches the code reads:
| Setting | When set | When unset |
|---|---|---|
TRIAL_SECRET | A signed cookie is written and the 5-page limit is enforced; responses include remaining. | No cookie is written, no page limit is applied, and remaining is null. |
OCR_PROVIDER_URL and OCR_API_KEY | Image-only PDFs are sent to that provider, and both values must be present for OCR to run. | Image-only PDFs end with ocr_not_configured (422). |
API_KEY | A request with the exact Authorization: Bearer header bypasses the trial and gets no cookie. | Every browser conversion is subject to the trial. |
DODO_STARTER_URL, DODO_PRO_URL, DODO_BUSINESS_URL | /api/checkout?plan=… redirects to the payment link. | The checkout endpoint returns checkout_not_configured (503). |
The Worker reports its own state at /api/health. When checked on 2026-10-30, the production deployment returned "ocr": false and "trial": true: image-only PDFs on that deployment stop at ocr_not_configured, and the page-count cookie is enforced. Treat the endpoint as the current source for those two flags. With ocr: false, no OCR provider was receiving statements from that deployment at the checked date.
What the code does not do
- No account, email capture or dashboard.
- No statement written to a database, object store, cache or queue in the conversion path.
- No AI or LLM step. Extraction is pdf.js text plus position and regex rules.
- No bank connection, ledger posting or transaction categorization.
- No currency conversion: the QBO export declares USD regardless of the statement's currency.
- No statement content in the URL, the cookie or a response header.
If you process client statements
Ask the operator which OCR provider is configured before sending scans, because the code is provider-agnostic and will not tell you. Confirm that the provider is acceptable for the client's data, and keep the original statement and the export in your own files, since nothing is retained here to retrieve later. The accountants page covers the review workflow; privacy is the policy.
Questions
Does the PDF leave my device?
Yes. It is uploaded to the conversion endpoint on this site's Cloudflare Worker and parsed there. There is no in-browser parsing mode.
Is my statement used to train a model?
There is no model or training step in the code. The only optional outbound call is to an OCR provider, and only when the deployment has one configured and the PDF has no text layer.
When is my data deleted?
There is no stored copy to delete. The request's memory is released when the response is built, the parsed rows stay in your tab until you reload, and the page-count cookie expires after 30 days or when you clear it.
What exactly is in the trial cookie?
An integer page count and an HMAC signature. No statement, transactions, file name, timestamp or user identifier.
Is there a history of my conversions?
No. There is no account and no server-side usage record; the page count in your browser is the only state kept.