Content
# paperless-tax-mcp
An **unofficial** MCP server and CLI for [paperless.tax](https://www.paperless.tax), the
Israeli bookkeeping/expense platform.
> ⚠️ Not affiliated with, endorsed by, or supported by Paperless. This talks to the app's
> own private backend, not a published developer API, and can break without notice.
> Use it against your own account.
The value here is as much [`docs/api-surface.md`](docs/api-surface.md) as the code — no
public documentation of this API exists anywhere, so the map is the point.
## Authentication
Use the **session JWT** the web app holds after login, as `Authorization: Bearer <jwt>`.
The self-service API key from the UI's API/webhooks modal is **not** a credential for these
endpoints — it returns 401 under every scheme, and paying for the API add-on does not change
that. Do not buy the add-on expecting it to help.
The good news, contrary to what you would assume of a "session" token: **its `exp` is about
ten months out**, so one interactive login covers scheduled work for most of a year. Login
is passwordless (email → entry code), so a fresh token needs a browser once; after that it
can be lifted from the browser profile.
[`docs/api-surface.md`](docs/api-surface.md#authentication--resolved) has both.
## Use as an MCP server
Register with any MCP client — Claude Code, Claude Desktop, or a gateway. No install step:
```json
{
"mcpServers": {
"paperless-tax": {
"command": "npx",
"args": ["-y", "paperless-tax-mcp"],
"env": { "PAPERLESS_TOKEN": "..." }
}
}
}
```
Claude Code, in one line:
```bash
claude mcp add paperless-tax --env PAPERLESS_TOKEN=... -- npx -y paperless-tax-mcp
```
## Use as a CLI
The same client, for exports and poking at the API. Install it once so the
`paperless-tax` binary is on your PATH:
```bash
npm install -g paperless-tax-mcp
export PAPERLESS_TOKEN=...
paperless-tax whoami # verify the token, print the account id
paperless-tax probe # hit each read endpoint, report what answers
paperless-tax list --type 1 # expenses only
paperless-tax list --from 2025-01 --to 2025-12
paperless-tax export --out export/ # files plus metadata sidecars
paperless-tax raw POST documents/get/downloadurl --json '{"sDocID":"..."}'
```
To run it without installing, `--package` is **required**:
```bash
npx --package paperless-tax-mcp paperless-tax export --out export/
```
Plain `npx paperless-tax …` would look for a *package* named `paperless-tax`, which is
not this one. And `npx paperless-tax-mcp paperless-tax …` is worse than an error: with two
binaries in the package, npx runs the one matching the package name — the MCP server —
treating `paperless-tax` as an argument. It waits on stdio, reads EOF, and exits 0 having
done nothing, which is indistinguishable from a successful run that exported nothing.
`export` writes `{docid}.pdf` plus a `{docid}.json` sidecar under
`export/expenses/{YYYY}/{YYYY-MM}/`, with a `manifest.jsonl` index. It is resumable —
documents already on disk are skipped — and the manifest is rewritten from scratch each run,
so it describes the whole export rather than only what that run fetched.
`--from`/`--to` accept `YYYY-MM-DD`, `YYYY-MM` or the API's native `YYMM`. Omit them to get
everything: the default range is the app's own unbounded `1001..2912`.
### Use as a library
```ts
import { PaperlessClient } from "paperless-tax-mcp/client";
import { Exporter } from "paperless-tax-mcp/export";
const client = new PaperlessClient({ token });
const docs = await client.byDate(); // whole history, one call
const pdfUrl = await client.downloadUrl(docs[0].id);
```
### Configuration
| Variable | Required | Meaning |
|---|---|---|
| `PAPERLESS_TOKEN` | yes | The session JWT — see above |
| `PAPERLESS_USER_ID` | no | 10-char account id; `whoami` discovers it |
| `PAPERLESS_BASE_URL` | no | Defaults to `https://api.paperless.tax/` |
### From source
```bash
npm install && npm run build && npm test
```
## Tools
| Tool | Endpoint | Confidence |
|---|---|---|
| `whoami` | `GET users/get/params` | confirmed live |
| `pending_documents` | `GET documents/get/pending/{uid}` | confirmed live |
| `list_documents` | `POST documents/get/bydate` | confirmed live |
| `get_document` | `GET documents/get/document/{id}` | confirmed live |
| `get_download_url` | `POST documents/get/downloadurl` | confirmed live |
| `raw_request` | anything | escape hatch for mapping new surface |
## Four things that will bite you
All documented at length in `docs/api-surface.md`, and all of them cost real time to find:
1. **The controller prefix is not optional.** `POST /bydate` is a 404; the route is
`POST documents/get/bydate`. Same for `downloadurl`, `searchdp`, `document/{id}`.
2. **A 401 does not mean the route exists.** Auth runs before routing, so unauthenticated
*every* path 401s — including nonsense ones. This is what produced the earlier, wrong
version of the endpoint map. Never infer existence from a 401 here.
3. **`bydate` takes `YYMM` period strings, not ISO dates.** `sStart: "2506"` is June 2025.
Give it a date and you get a narrower result set rather than an error.
4. **A 204 is not success.** Send no credentials at all and you get the same 204. Control-test
every auth scheme against a deliberately invalid credential.
Also worth knowing: `documents/get/bydate` returns an entire account history in one
unpaginated response, and its records are identical to what the per-document detail endpoint
returns — so a per-document loop is pure waste. And `export/download` with
`iExportType: 8` (Backup) answers 503 `NotImplementedException`; there is no whole-account
backup on that route.
## Contributing
Corrections to `docs/api-surface.md` are the most valuable contribution — especially
anything that moves a row from *inferred* to *confirmed*. Please mark what you verified
versus what you assumed, and never include account identifiers, tokens, or ingest addresses
in an issue or PR.
## Licence
MIT.