The official client for the TrueUp API. Send TrueUp two ledgers (a supplier statement and your receiving log, your books and the bank feed, invoices and payments) and it pairs every row, then tells you what's only on one side, what was counted twice and where the numbers disagree.
Python 3.9+. One dependency (httpx).
pip install trueupCreate an API key in the TrueUp dashboard (API keys), then:
export TRUEUP_API_KEY=tu_live_...from trueup import TrueUp
trueup = TrueUp() # reads TRUEUP_API_KEY
result = trueup.reconcile("statement.csv", "receiving.csv") # left: the side that bills or claims
print(result["headline"])
# 7 of 8 rows of statement.csv paired with receiving.csv; 1 only in statement.csv, ...
for f in result["findings"]:
print(f["kind"], f["subject"], f["detail"], f["amount"])
# qty_mismatch statement.csv:row 5 Qty 24 vs qty_received 20; ... 99.6
# phantom statement.csv:row 6 no match on the other side 43.2A table is a path, file contents, or rows:
from trueup import Table
trueup.reconcile("books.csv", "bank.csv")
trueup.reconcile(Table.content("books.csv", csv_text), Table.content("bank.csv", csv_bytes))
trueup.reconcile(
Table.rows("invoices", [{"Invoice #": "INV-10101", "Date": "2026-06-09", "Total": "$2,999.31"}]),
Table.rows("payments", [{"Received": "2026-07-01", "From": "ACME CONSTR", "Amount": "2999.31"}]),
)CSV, TSV, JSON and JSON Lines are read, and date and number formats are detected. Nothing about the columns is configured.
Not sure which file is which? Send them all and TrueUp picks the pair and the sides:
trueup.reconcile_files(["a.csv", "b.csv"])Reuse what was learned. Every result carries details["weights"]. Pass them back to reconcile next month's files the same way, without learning again:
march = trueup.reconcile("march-statement.csv", "march-receiving.csv")
april = trueup.reconcile("april-statement.csv", "april-receiving.csv", weights=march["details"]["weights"])Answer the questions. Findings with status == "unsure" need a person. Send the decisions back:
trueup.reconcile("statement.csv", "receiving.csv", answers={
"same": [["statement.csv:row 12", "receiving.csv:row 11"]],
"different": [["statement.csv:row 3", "receiving.csv:row 9"]],
})Each call to reconcile or reconcile_files counts as one analysis on your plan.
Two lists that describe the same things in different words (two catalogs, a supplier's price book and your invoice, two vendor lists): every record on the left is paired with its counterpart on the right, or reported as having none. Nothing is configured; the columns can have different names.
result = trueup.match("invoice.csv", "catalog.csv")
print(result["headline"])
# 4 of 5 records in invoice.csv matched to catalog.csv (0 unsure); 1 have no counterpart.
for f in result["findings"]:
print(f["kind"], f["subject"], f["confidence"], f["detail"])
# match 4 ~ 5 0.965 4 · cheese puffs jumbo 8oz · 3.30 · 10 ↔ C-105 · Cheese Puffs Jumbo 8 oz · 3.25
# only_left 5 None 5 · beef jerky teriyaki 2.5oz · 5.75 · 6kind is match, unsure_match (a person should check), only_left or only_right. details["pairs"] lists [left id, right id, confidence]. Like reconcile, it takes paths or Tables; match_files([...]) picks the pair; match_stored(left_id, right_id, model=...) works on stored files; and details["weights"] can be passed back as weights= to match next month's lists the same way. One analysis per call.
Find what doesn't add up. Send text documents with labeled amounts (invoices, statements, schedules; about 4 or more of a kind) and TrueUp learns the arithmetic each kind obeys from the documents themselves, then flags the ones that break it. Send one table and it checks its rows the same way (qty × unit price = amount), and flags repeated rows.
result = trueup.audit([f"inv-104{i}.txt" for i in range(1, 7)])
print(result["headline"])
# 1 of 6 documents don't add up; 0 more to review (5 laws learned).
for f in result["findings"]:
print(f["subject"], f["amount"], f["detail"])
# inv-1045.txt 200 subtotal + tax amount = total: 4,837.84 vs 5,037.84
# Next month, even one invoice at a time, against the same laws:
trueup.audit(["inv-1050.txt"], weights=result["details"]["weights"])audit_stored(file_ids, model=...) audits stored files. One analysis per call.
Price a new job from your past estimates. Send a domain file for the trade (a .tu file naming the facts to read, what costs scale with, and the cost categories), at least 3 past estimates in any format (CSV, TSV, Markdown, JSON, or text proposals), and one request describing the new job in plain words:
result = trueup.estimate(["barndo.tu", "01_anderson.csv", "02_brooks.csv", "03_carter.md", ..., "job_a.txt"])
print(result["headline"])
# job_a.txt: $292,267 (80% range $248,742 – $335,792) from 10 past estimates.
for f in result["findings"]:
if f["kind"] == "priced_line":
print(f["subject"], f["amount"], f["detail"])
# The next job, with what was learned (no need to send the history again):
trueup.estimate(["job_b.txt"], weights=result["details"]["weights"])estimate_stored(file_ids, model=...) prices from stored files. One analysis per call.
Files uploaded to your team stay there (you'll also see them in the dashboard). Runs on stored files are kept, and what a run learned can be saved as a model:
statement, receiving = trueup.files.upload("statement.csv", "receiving.csv")
statement["rows"] # 8
statement["roles"] # {"Inv Date": "date", "Qty": "number", ...}
result = trueup.reconcile_stored(statement["id"], receiving["id"])
model_id = trueup.models.create(result["run_id"], "Acme statements")
# Next month: apply what was learned.
trueup.reconcile_stored(file_ids=[april_statement["id"], april_receiving["id"]], model=model_id)| Call | Returns |
|---|---|
files.upload(*tables), files.list(), files.get(id) |
stored files: id, name, rows, columns, roles |
files.content(id) |
the bytes, exactly as uploaded |
files.delete(id) |
|
reconcile_stored(left_id, right_id) or reconcile_stored(file_ids=[...]), with model=, answers= |
a result plus run_id (one analysis) |
runs.list(limit=, before=) |
{"runs": [...], "has_more": bool}, newest first |
runs.all() |
every run (a generator that pages for you) |
runs.get(id) |
{"run": ..., "result": ...} |
models.create(run_id, name), models.list(), models.get(id), models.delete(id) |
models.get includes the weights |
kind |
Meaning |
|---|---|
phantom |
Only on the left: billed or recorded, never matched |
unbilled |
Only on the right: received or paid, never billed |
duplicate, received_duplicate |
A copy of a row that's already paired |
qty_mismatch, price_change, amount_mismatch |
Paired rows whose numbers disagree |
unsure_pair |
A likely pair a person should confirm |
trueup.account() # {"team": ..., "plan": ..., "key": ...}
trueup.usage() # {"period": ..., "metrics": [{"metric": "analyses", "used": 2, "included": 50, ...}]}
trueup.plans()Every error is a TrueUpError with status, code (the API's error code) and message:
| Class | When |
|---|---|
AuthenticationError |
401: missing, unknown or revoked key |
InvalidRequestError |
400, 413, 415, 422: the request or the files need fixing (unsupported_file, not_reconcilable, ...) |
RateLimitError |
429 rate_limited: retried automatically; retry_after seconds |
QuotaExceededError |
429 quota_exceeded: the plan's monthly allowance is used up |
ServerError |
5xx: retried automatically |
ConnectionError |
the API couldn't be reached |
from trueup import QuotaExceededError
try:
trueup.reconcile("a.csv", "b.csv")
except QuotaExceededError as e:
print("Upgrade the plan:", e.message)TrueUp(
api_key="tu_live_...", # default: TRUEUP_API_KEY
base_url="https://...", # default: TRUEUP_BASE_URL, then the hosted API
timeout=300, # seconds per request
max_retries=2, # rate limits, 5xx and dropped connections
)TrueUp is a context manager (with TrueUp() as trueup:) and closes its connection pool on exit.
The tests run in Docker against the live API:
export TRUEUP_API_KEY=tu_live_... # a key for a test team (each run uses 10 analyses)
just test # or: docker compose run --rm testMIT