|
1 | 1 | # simplifiapi |
2 | | -An unofficial API for Quicken Simplifi |
| 2 | + |
| 3 | +An unofficial CLI and Python library for extracting your own Quicken Simplifi data — accounts, transactions, tags, and categories — to JSON or CSV. |
| 4 | + |
| 5 | +## Security & Trust Model |
| 6 | + |
| 7 | +This is an **unofficial** client for Quicken Simplifi. Before running it against your real account, understand the trust model: |
| 8 | + |
| 9 | +- **OAuth client secret comes from your environment.** The OAuth client secret is no longer embedded in source. You must set `SIMPLIFI_CLIENT_SECRET` in your environment before invoking the CLI or the `Client` class. If it is unset, the tool fails loudly rather than silently proceeding. |
| 10 | +- **Your Simplifi account password is never accepted on argv.** The `--password` CLI flag has been removed (it leaked via `ps`, shell history, and syslog). Provide your password by either setting `SIMPLIFI_PASSWORD` in your environment, or by letting the CLI prompt you via `getpass.getpass()` when run interactively. Non-interactive runs with no env var exit with a clear error. |
| 11 | +- **MFA codes are read via `getpass.getpass()`** — they do not echo to the terminal and do not enter readline history. |
| 12 | +- **The bearer token, refresh token, and Simplifi user id are never logged** at any level. |
| 13 | +- **HTTP calls are timeout-bounded and host-guarded.** Every request carries a 30-second timeout. The `nextLink` value returned by Quicken during pagination is validated: it must be either a relative path or an absolute URL on `https://services.quicken.com/`. Anything else is rejected before the bearer token is sent. |
| 14 | +- **`--filename` cannot escape the working directory.** Any path components in the value are stripped via `os.path.basename` before opening the output file. |
| 15 | +- **CSV output is formula-injection-safe.** Cells whose stringified value starts with `=`, `+`, `-`, or `@` are prefixed with a single quote before writing. |
| 16 | +- **The OAuth `clientSecret` cannot be rotated per-user.** Because this is an unofficial client, Quicken does not own a per-user relationship for the OAuth `clientSecret` and cannot revoke it for an individual user. Anyone running this client is implicitly accepting that the secret is shared across every user of the project. If the secret leaks, every user is affected — Quicken's only remediation would be invalidating the secret for the entire client, breaking the tool for everyone. |
| 17 | +- **Run in an isolated environment.** Treat `SIMPLIFI_CLIENT_SECRET` and `SIMPLIFI_PASSWORD` as sensitive. Prefer a dedicated shell, container, or VM where those env vars do not bleed into your normal development environment or any background process that snapshots `/proc/<pid>/environ`. Export them via a sourced `.env` file you do not commit, not by typing them at a prompt that lands in shell history. |
| 18 | + |
| 19 | +A starter `.env.example` is shipped at the repo root. Copy it to `.env` and fill in your values; do not commit `.env`. |
3 | 20 |
|
4 | 21 | ## Install |
5 | 22 |
|
6 | | -PyPI is temporarily down. Install with pip from GitHub directly |
| 23 | +Install from a clone of this repo: |
7 | 24 |
|
8 | 25 | ```shell |
9 | | -pip3 install git+https://github.com/rijn/simplifiapi |
| 26 | +# from a clone of this repo |
| 27 | +pip install . |
| 28 | + |
| 29 | +# or, in editable mode for local development |
| 30 | +pip install -e . |
| 31 | + |
| 32 | +# or, with dev/test extras (pytest + responses) |
| 33 | +pip install -e '.[dev]' |
10 | 34 | ``` |
11 | 35 |
|
| 36 | +Pinned runtime dependencies (from `pyproject.toml`): `requests>=2.31,<3`, `pandas>=2,<3`, `configargparse>=1.7,<2`. Python 3.9+. |
| 37 | + |
12 | 38 | ## CLI |
13 | 39 |
|
14 | | -This package provides a command-line tool that could access and save data to local files. |
| 40 | +The package installs a `simplifiapi` entry point that extracts data from your Simplifi account to local files. |
| 41 | + |
| 42 | +```shell |
| 43 | +usage: simplifiapi [-h] [--email [EMAIL]] [--token [TOKEN]] [--accounts] |
| 44 | + [--transactions] [--tags] [--categories] |
| 45 | + [--filename FILENAME] [--format {json,csv}] |
| 46 | + |
| 47 | +simplifiapi — extract Quicken Simplifi data to JSON or CSV. Requires the |
| 48 | +SIMPLIFI_CLIENT_SECRET environment variable to be set (the OAuth client secret |
| 49 | +is no longer embedded in source — see README.md > Security & Trust Model). |
| 50 | + |
| 51 | +options: |
| 52 | + -h, --help show this help message and exit |
| 53 | + --email [EMAIL] The e-mail address for your Quicken Simplifi account |
| 54 | + --token [TOKEN] Use existing token to bypass MFA check |
| 55 | + --accounts Retrieve accounts |
| 56 | + --transactions Retrieve transactions |
| 57 | + --tags Retrieve tags |
| 58 | + --categories Retrieve categories |
| 59 | + --filename FILENAME Write results to file with this prefix (path |
| 60 | + components stripped — see Security & Trust Model) |
| 61 | + --format {json,csv} The format used to return data. |
| 62 | +``` |
| 63 | +
|
| 64 | +### Examples |
15 | 65 |
|
16 | 66 | ```shell |
17 | | -usage: simplifiapi [-h] [--email [EMAIL]] [--password [PASSWORD]] [--token [TOKEN]] [--accounts] [--transactions] [--tags] [--categories] [--filename FILENAME] [--format {json,csv}] |
18 | | - |
19 | | -optional arguments: |
20 | | - -h, --help show this help message and exit |
21 | | - --email [EMAIL] The e-mail address for your Quicken Simplifi account |
22 | | - --password [PASSWORD] |
23 | | - The password for your Quicken Simplifi account |
24 | | - --token [TOKEN] Use existing token to bypass MFA check |
25 | | - --accounts Retrieve accounts |
26 | | - --transactions Retrieve transactions |
27 | | - --tags Retrieve tags |
28 | | - --categories Retrieve categories |
29 | | - --filename FILENAME Write results to file this prefix |
30 | | - --format {json,csv} The format used to return data. |
31 | | - |
32 | | -examples: |
33 | | -> simplifiapi --token="..." --transactions |
34 | | -> simplifiapi --token="..." --transactions --filename=20231125 --format=csv |
| 67 | +# Interactive login: getpass prompts for password (and MFA code if challenged). |
| 68 | +SIMPLIFI_CLIENT_SECRET=... simplifiapi --email=you@example.com --transactions |
| 69 | + |
| 70 | +# Non-interactive: password from env, scrape to CSV with a date prefix. |
| 71 | +SIMPLIFI_CLIENT_SECRET=... SIMPLIFI_PASSWORD=... \ |
| 72 | + simplifiapi --email=you@example.com --transactions --filename=20251125 --format=csv |
| 73 | + |
| 74 | +# Reuse an existing bearer token (skip OAuth + MFA). |
| 75 | +SIMPLIFI_CLIENT_SECRET=... simplifiapi --token="..." --accounts --transactions |
35 | 76 | ``` |
36 | 77 |
|
| 78 | +Output files are written to the current working directory as `{filename}_{resource}.{format}` (e.g. `output_transactions.csv`). Any path separators or `..` segments in `--filename` are stripped before the file is opened. |
| 79 | +
|
| 80 | +### Exit codes & errors |
| 81 | +
|
| 82 | +The CLI catches `SimplifiAPIError` (and its `AuthenticationError` subclass) at the boundary and exits with a one-line `simplifiapi: <message>` instead of a Python traceback. Missing `SIMPLIFI_CLIENT_SECRET` is treated as a developer-environment error and fails loudly with a `RuntimeError` so it is not silently miscategorised as an auth failure. |
| 83 | +
|
| 84 | +### Logging |
| 85 | +
|
| 86 | +Set `LOG_LEVEL=DEBUG` to see the per-request pagination log. Bearer tokens, refresh tokens, MFA channels, and user IDs are never logged. |
| 87 | +
|
37 | 88 | ## Python API |
38 | 89 |
|
39 | | -The `Client` class allows accessing from python script and making custom analysis. |
| 90 | +The `Client` class lets you call the same endpoints from your own scripts. Importing the package is logging-pure — it attaches only a `NullHandler` to the `simplifiapi` logger and does not touch root-level logging. |
40 | 91 |
|
41 | 92 | ```python |
| 93 | +import os |
| 94 | +from simplifiapi import AuthenticationError, SimplifiAPIError |
42 | 95 | from simplifiapi.client import Client |
43 | 96 |
|
| 97 | +# SIMPLIFI_CLIENT_SECRET must be set in the environment before calling get_token. |
| 98 | +# When calling Client directly, your code passes `password=` explicitly — |
| 99 | +# resolve it however you want (getpass, keyring, etc.), just don't read it |
| 100 | +# from sys.argv. |
| 101 | +os.environ["SIMPLIFI_CLIENT_SECRET"] = "<your-client-secret>" |
| 102 | + |
44 | 103 | client = Client() |
45 | 104 |
|
46 | | -# Provide either token or email/password |
47 | | -token = "..." |
48 | | -token = client.get_token(email=options.email, password=options.password) |
| 105 | +try: |
| 106 | + # Option A: full OAuth + (optional) MFA flow. |
| 107 | + token = client.get_token(email="you@example.com", password="<resolved-via-env-or-getpass>") |
| 108 | + |
| 109 | + # Option B: reuse an existing bearer token instead of get_token. |
| 110 | + # token = "..." |
| 111 | + |
| 112 | + # verify_token raises AuthenticationError on a bad token and installs the |
| 113 | + # bearer header on the session on success. It does not return a value. |
| 114 | + client.verify_token(token) |
| 115 | + |
| 116 | + # Datasets own transactions and accounts. |
| 117 | + datasets = client.get_datasets() |
| 118 | + if not datasets: |
| 119 | + raise SystemExit("No datasets found for this account") |
| 120 | + dataset_id = datasets[0]["id"] |
| 121 | + |
| 122 | + # All four getters return list[dict] of fully-unpaginated resources. |
| 123 | + transactions = client.get_transactions(dataset_id) |
| 124 | + accounts = client.get_accounts(dataset_id) |
| 125 | + tags = client.get_tags(dataset_id) |
| 126 | + categories = client.get_categories(dataset_id) |
| 127 | +except AuthenticationError as exc: |
| 128 | + # OAuth / token-verify failure. Typed subclass of SimplifiAPIError. |
| 129 | + raise SystemExit(f"auth failed: {exc}") |
| 130 | +except SimplifiAPIError as exc: |
| 131 | + # Pagination, JSON-decode, or unsafe nextLink rejection. |
| 132 | + raise SystemExit(f"api error: {exc}") |
| 133 | +``` |
| 134 | +
|
| 135 | +### Exception hierarchy |
| 136 | +
|
| 137 | +Both exception types are re-exported from the package root: |
49 | 138 |
|
50 | | -assert client.verify_token(token) |
| 139 | +- `SimplifiAPIError` — base class for all boundary errors raised by `Client` (bad HTTP status on a resource fetch, invalid JSON in a response, refusal to follow an unsafe `nextLink`). |
| 140 | +- `AuthenticationError` (subclass of `SimplifiAPIError`) — OAuth authorize/token failure, MFA mismatch, or `verify_token` failure. |
51 | 141 |
|
52 | | -# Datasets own transactions and accounts |
53 | | -datasets = client.get_datasets() |
54 | | -datasetId = datasets[0]["id"] |
| 142 | +`RuntimeError` is raised separately when `SIMPLIFI_CLIENT_SECRET` is missing; it is intentionally not a `SimplifiAPIError` so the CLI does not catch and prettify it. |
55 | 143 |
|
56 | | -# Access transactions |
57 | | -transactions = client.get_transactions(datasetId) |
| 144 | +## Development |
| 145 | +
|
| 146 | +```shell |
| 147 | +pip install -e '.[dev]' |
| 148 | +pytest # run the test suite |
| 149 | +pytest --cov # with coverage (gate is fail_under=80) |
| 150 | +pytest -k unit # only unit-marked tests |
58 | 151 | ``` |
59 | 152 |
|
| 153 | +The test suite uses `pytest` + the `responses` library to mock the Quicken HTTP surface. Tests live under [tests/](tests/). |
| 154 | +
|
60 | 155 | ## Thanks |
61 | 156 |
|
62 | | -This library is heavily inspired by [mintapi](https://github.com/mintapi/mintapi). |
| 157 | +This library is heavily inspired by [mintapi](https://github.com/mintapi/mintapi). |
0 commit comments