TrueUp API
The TrueUp API is a JSON API over HTTPS at https://trueup-cloud.merchantprotocol.workers.dev/v1. You authenticate with a secret API key that belongs to a team; every call is metered against that team's plan.
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, with how sure it is and which rows each finding came from. See POST /v1/reconcile. Send it two lists that describe the same things in different words (two catalogs, a price book and an invoice) and it pairs each record with its counterpart: POST /v1/match. Send it invoices and statements and it finds the ones whose numbers don't add up: POST /v1/audit. Send it your past estimates and a new job, and it prices the job: POST /v1/estimate. The account, usage and plan endpoints tell you where your team stands.
You can also upload files to the team and reconcile them by id, keep every run, and save what TrueUp learned as a model for next month. Try any call with your own key in the API playground, or do it without code on the Overview.
API version 2026-09-26.
Quickstart
- Create a free account. You get a team on the free plan straight away, no card needed.
- Open API keys and create a key. It's shown once; copy it.
- Make your first call:
export TRUEUP_API_KEY="tu_live_…"
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/account \
-H "Authorization: Bearer $TRUEUP_API_KEY"The same call from JavaScript and Python:
const res = await fetch("https://trueup-cloud.merchantprotocol.workers.dev/v1/usage", {
headers: { Authorization: `Bearer ${process.env.TRUEUP_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());import os, requests
res = requests.get("https://trueup-cloud.merchantprotocol.workers.dev/v1/usage",
headers={"Authorization": f"Bearer {os.environ['TRUEUP_API_KEY']}"})
res.raise_for_status()
print(res.json())Authentication
Send your key in the Authorization header on every request:
Authorization: Bearer tu_live_4Qh…- Keys start with
tu_live_followed by 40 random characters. - A key is shown once, when it's created. TrueUp stores only a SHA-256 fingerprint, so a lost key can't be recovered: revoke it and create a new one.
- Keys are secrets. Call the API from your server, never from a browser or mobile app, and keep keys out of source control.
- A missing, malformed, unknown or revoked key gets
401.
Teams, keys and access
Everything in TrueUp belongs to a team: API keys, usage, the plan and billing. One login can belong to many teams (for example your company and each client you run TrueUp for) and switch between them in the dashboard.
| Role | Can |
|---|---|
| Owner | Everything, including billing, roles, renaming the team and removing owners. A team always has at least one owner. |
| Admin | Invite and remove members, revoke any key, plus everything a member can do. |
| Member | Create keys, revoke their own keys, see usage and the request log. |
A key acts for its team, not for the person who made it: it keeps working if that person leaves. Revoke keys when someone who had access to them leaves.
Requests and responses
- Base URL:
https://trueup-cloud.merchantprotocol.workers.dev/v1. HTTPS only. - Responses are JSON (
application/json; charset=utf-8) and never cached. - Times are ISO 8601 in UTC. Money is in integer cents unless a field says otherwise.
- CORS is open so you can try calls from tools, but keys still belong on your server.
Every successful response carries your usage so far this month:
X-TrueUp-Requests-Used: 1532
X-TrueUp-Requests-Included: 10000Errors
Errors use standard HTTP status codes and always have the same body:
{
"error": {
"code": "quota_exceeded",
"message": "This team has used all 10000 API requests included this month. …"
}
}Branch on error.code; the message is for people and may change.
| Status | code | Meaning and what to do |
|---|---|---|
| 400 | invalid_request | The request body is malformed or a parameter is missing. The message says which. |
| 401 | missing_api_key | No Authorization: Bearer header. |
| 401 | invalid_api_key | The key is malformed, unknown or revoked. Create a new one. |
| 404 | not_found | No endpoint at that path. |
| 405 | method_not_allowed | The path exists but not with that HTTP method. |
| 413 | payload_too_large, too_many_rows | Over the size limits of reconcile or match. Split the files. |
| 415 | unsupported_media_type | Send multipart/form-data (files) or application/json (rows). |
| 422 | unsupported_file, unreadable_file, not_a_table, empty_table | A file TrueUp can't read as a table. Send CSV, TSV, JSON rows, JSON Lines or a PDF statement. |
| 422 | unsupported_file (invalid_input from POST /v1/files) with reason pdf_scanned, pdf_garbled, pdf_encrypted, pdf_damaged or pdf_too_large | A PDF TrueUp can't read: a scan on which OCR finds no text, a text layer with almost no words, a password, a damaged file, or over 400 pages (60 scanned). error.reason says which, and the message explains it. See PDF files. |
| 503 | ocr_unavailable | A scanned page couldn't be read just now (the OCR model was busy or failed). Nothing was counted: send it again in a minute. |
| 422 | not_reconcilable, not_matchable, not_auditable, not_estimable, weights_mismatch | The files don't line up (for reconcile: no dates and shared amounts; for match: no shared words; for audit: too few documents of a kind; for estimate: no domain file, too few past estimates, or not exactly one request), or saved weights don't fit these columns or this analysis. Not counted as a check. |
| 429 | rate_limited | Too many requests this minute for this key. Wait for Retry-After seconds and retry. |
| 429 | quota_exceeded | The team used everything its plan includes this month. Upgrade, or wait for the monthly reset. Retrying won't help. |
| 500 | server_error | Our fault. It's logged; retry with backoff. |
Rate limits
Each key can make up to 600 requests per minute. Past that you get 429 rate_limited with a Retry-After header. Retry with exponential backoff (for example 1s, 2s, 4s, capped at 30s). Requests refused by the rate limit don't count toward your monthly usage.
Usage and quotas
Usage is counted per team, per calendar month in UTC, and resets at 00:00 UTC on the 1st. What's metered:
| Metric | Counts |
|---|---|
api_requests | Every authenticated call to the /v1 API. |
analyses | Each time TrueUp learns from a set of files and checks them. |
Each plan includes an amount of each metric. On plans with a hard limit (the free plan), use past the limit is refused with 429 quota_exceeded. On paid plans, use past the included amount keeps working and is billed at the plan's overage rate. See pricing, or read your live numbers from GET /v1/usage.
Endpoints
GET /v1
Checks your key and returns the API version.
{ "service": "TrueUp API", "version": "2026-09-26", "docs": "/docs" }GET /v1/account
The team, plan and key behind this request.
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/account -H "Authorization: Bearer $TRUEUP_API_KEY"{
"team": { "id": "team_8FJ2…", "name": "Acme Plumbing" },
"plan": { "slug": "free", "name": "Free" },
"key": { "id": "key_Zq1…", "name": "Production server", "prefix": "tu_live_4Qh8kLm2" }
}GET /v1/usage
This month's usage for the key's team, per metric.
{
"period": "2026-09",
"resets_at": "2026-10-01T00:00:00.000Z",
"metrics": [
{ "metric": "api_requests", "label": "API requests", "used": 1532, "included": 10000,
"remaining": 8468, "hard_cap": true, "overage": 0 },
{ "metric": "analyses", "label": "Checks", "used": 0, "included": 3,
"remaining": 3, "hard_cap": true, "overage": 0 }
]
}| Field | Meaning |
|---|---|
used | Units counted this month, including this request. |
included | What the plan includes each month. |
remaining | included − used, never below 0. |
hard_cap | true: use past included is refused. false: it's billed as overage. |
overage | Units past included this month (paid plans). |
GET /v1/plans
The plans you can be on, with what each includes.
{
"plans": [
{ "slug": "free", "name": "Free", "price_cents": 0, "interval": "month", "purchasable": true,
"limits": [ { "metric": "api_requests", "included": 10000, "hard_cap": true, "overage_micros_per_unit": null } ] }
]
}overage_micros_per_unit is the price per unit past the included amount in millionths of a US dollar (5 = $5 per million).
POST /v1/reconcile
Reconciles two tables of events: pairs every row one-to-one and explains every leftover and disagreement. Nothing about the columns is configured: TrueUp works out which columns are dates, amounts, quantities, references and descriptions from their values, which columns line up across the two files (including qty × unit cost against an extended amount, and split Debit/Credit columns), and learns from the files themselves how much each kind of agreement means "the same event". Each call counts as one analysis.
Send two files as multipart/form-data: left is the side that bills or claims (the statement, your books), right the other side. CSV, TSV, JSON (a list of rows), JSON Lines and PDF statements are read; the delimiter and the date and number formats are detected. Rows of a PDF are named by page and line: statement.pdf:page 2 line 14.
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/reconcile \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-F left=@statement.csv \
-F right=@receiving.csvOr send both as files and TrueUp picks the pair and the sides. Or send rows as JSON, one object per row:
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/reconcile \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"left": { "name": "statement.csv", "rows": [ { "Inv Date": "08/03/2026", "Item": "hot chips fuego 17oz", "Qty": "24", "Amount": "$99.60" } ] },
"right": { "name": "receiving.csv", "rows": [ { "received": "2026-08-03", "item": "Hot Chips Fuego 17 oz", "qty_received": "20", "po_cost": "4.15" } ] }
}'| Parameter | Meaning |
|---|---|
left, right | The two tables: files (multipart), or { name, rows } (JSON). The names are how findings refer to rows: statement.csv:row 5. |
files | Multipart only: two or more files; TrueUp proposes the best pair and puts the billing side on the left. |
weights | Optional. The details.weights of an earlier result: apply what was learned then instead of learning again (same columns required). JSON text in multipart. |
answers | Optional. Decisions a person made: { "same": [[left row, right row]], "different": [[left row, right row]] }. "Different" pairs never pair; "same" rows pair only with each other. |
The response:
{
"analysis": "reconcile",
"title": "Reconcile statement.csv with receiving.csv",
"headline": "7 of 8 rows of statement.csv paired with receiving.csv; 1 only in statement.csv, 0 only in receiving.csv, 1 with differing numbers, 0 unsure.",
"stats": { "paired": 7, "unsure": 0, "only in statement.csv": 1, "only in receiving.csv": 0,
"with differing numbers": 1, "statement.csv amount with no counterpart": 43.2 },
"findings": [
{ "kind": "qty_mismatch", "label": "quantity differs", "subject": "statement.csv:row 5",
"detail": "Qty 24 vs qty_received 20; Amount 99.60 vs qty_received x po_cost 83",
"confidence": 0.98, "status": "yes", "amount": 99.6,
"provenance": ["statement.csv:row 5", "receiving.csv:row 5"], "data": { "left": {…}, "right": {…} } },
{ "kind": "phantom", "label": "only in statement.csv", "subject": "statement.csv:row 6",
"detail": "no match on the other side", "confidence": null, "status": "yes", "amount": 43.2,
"provenance": ["statement.csv:row 6"], "data": { "row": {…} } }
],
"details": { "model": {…}, "weights": {…}, "pairs": [ { "left": "statement.csv:row 1", "right": "receiving.csv:row 1", "confidence": 0.996 } ] },
"inputs": ["statement.csv", "receiving.csv"],
"engine": "0.1.0"
}Finding kind | Meaning |
|---|---|
phantom | Only on the left: billed or recorded, never matched on the other side. |
unbilled | Only on the right: received or paid, never billed. |
duplicate, received_duplicate | A copy of a row that is already paired (a re-bill, a second posting), on the left or the right. |
qty_mismatch, price_change, amount_mismatch | Paired rows whose aligned numbers disagree. |
unsure_pair | A likely pair TrueUp isn't sure of (status: "unsure"): a person should decide, and can send the decision back as answers. |
Findings are ordered decided-first, largest amount first. confidence is how likely the pairing is right (null for rows with no pair). Limits: 25 MB per request and 20,000 rows per side. Successful responses also carry X-TrueUp-Analyses-Used and X-TrueUp-Analyses-Included. Your files are read in memory to answer the request and not stored.
POST /v1/match
Matches two lists that describe the same things in different words: two product 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 (one to one), or reported as having none, with how sure TrueUp is. Nothing is configured: TrueUp finds the columns that describe the things (and the prices) in each list, even when they're named differently, and learns from the two lists themselves what makes two records the same thing. Each call counts as one analysis.
It takes the same inputs as reconcile: two files as left and right (left is the list you're going through, right the one to search), two or more as files (TrueUp picks the pair and puts the shorter list on the left), rows as JSON, or uploaded files by id. weights applies an earlier match's details.weights; answers isn't used.
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/match \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-F left=@invoice.csv \
-F right=@catalog.csv{
"analysis": "match",
"title": "Match invoice.csv to catalog.csv",
"headline": "4 of 5 records in invoice.csv matched to catalog.csv (0 unsure); 1 have no counterpart.",
"stats": { "pairs": 4, "unsure pairs": 0, "only in invoice.csv": 1, "only in catalog.csv": 2 },
"findings": [
{ "kind": "match", "label": "same thing", "subject": "4 ~ 5",
"detail": "4 · cheese puffs jumbo 8oz · 3.30 · 10 ↔ C-105 · Cheese Puffs Jumbo 8 oz · 3.25",
"confidence": 0.965, "status": "yes", "amount": null,
"provenance": ["invoice.csv:row 4", "catalog.csv:row 5"], "data": { "left_id": "4", "right_id": "5" } },
{ "kind": "only_left", "label": "only in invoice.csv", "subject": "5",
"detail": "5 · beef jerky teriyaki 2.5oz · 5.75 · 6", "confidence": null, "status": "yes", "amount": null,
"provenance": ["invoice.csv:row 5"], "data": { "left_id": "5" } }
],
"details": { "columns": { "left": { "Item Description": "title", "Unit Cost": "price", … }, "right": { "name": "title", … } },
"pairs": [ ["1", "1", 0.984], ["4", "5", 0.965] ], "model": { "learned": true }, "weights": {…} },
"inputs": ["invoice.csv", "catalog.csv"]
}kind | Meaning |
|---|---|
match | The same thing on both lists (status: "yes"). |
unsure_match | Probably the same thing; a person should check (status: "unsure"). |
only_left, only_right | A record with no counterpart on the other list. |
Records are named by their list's own id column when it has a unique one (id, key, sku_id…), otherwise by row number; provenance always names the row. Unsure matches come first, then matches by confidence, then what has no counterpart. Past 2,000 records on the right with no counterpart, those are counted in stats but not listed. Limits: 25 MB per request, 20,000 records in the two lists together, and 60,000,000 for left × right (2,500 against 17,500 takes about 25 seconds). A pair of lists with nothing in common gets 422 not_matchable and isn't counted.
POST /v1/audit
Finds what doesn't add up. Send text documents with labeled amounts (invoices, statements, schedules: Subtotal: $4,466.80, - Description Ridge cap | Amount $256.80) and TrueUp learns the arithmetic each kind of document obeys from the documents themselves (subtotal + tax = total, the lines add up to the subtotal, tax = subtotal × rate, opening balance + movements = closing balance; across documents that cite the same reference too), then flags every document that breaks it. It needs about 4 documents of a kind. Send one table instead and it does the same for its rows (qty × unit price = amount, subtotal + tax = total), and flags rows that repeat an earlier row. Each call counts as one analysis.
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/audit \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-F files=@inv-1041.txt -F files=@inv-1042.txt -F files=@inv-1043.txt \
-F files=@inv-1044.txt -F files=@inv-1045.txt -F files=@inv-1046.txtOr as JSON: {"documents": [{"name": "inv-1041.txt", "text": "INVOICE\nSubtotal: $4,466.80\n…"}, …]}, or {"table": {"name": "orders.csv", "rows": [ {…}, … ]}}, or uploaded files as {"file_ids": [ … ], "model": "model_…"}. weights applies an earlier audit's details.weights, so a single new document can be checked against laws learned before.
{
"analysis": "audit",
"title": "Audit 11 documents",
"headline": "2 of 11 documents don't add up; 0 more to review (6 laws learned).",
"stats": { "documents": 11, "kinds": 2, "laws learned": 6, "documents that don't add up": 2, "sent to review": 0 },
"findings": [
{ "kind": "arithmetic", "label": "doesn't add up", "subject": "inv-1045.txt",
"detail": "subtotal + tax amount = total: 4,837.84 vs 5,037.84", "confidence": 0.857, "status": "yes", "amount": 200,
"provenance": ["inv-1045.txt"],
"data": { "law": "subtotal + tax amount = total", "left": 4837.84, "right": 5037.84, "difference": -200, "support": "held in 5 of 6 invoice" } }
],
"details": { "laws": [ { "scope": "invoice", "law": "subtotal + tax amount = total", "held": "5 of 6" }, … ], "model": { "learned": true }, "weights": {…} }
}kind is arithmetic (the numbers break a law; amount is how far off) or, for a table, duplicate_row. A table's result is "analysis": "table-audit" with rows, rows that don't add up and repeated rows in stats. A law that held in fewer of the other documents sends its breaks to review (status: "unsure"). Nothing to audit gets 422 not_auditable and isn't counted.
POST /v1/estimate
Prices a new job from your past estimates. Send three kinds of files together: a domain file for the trade (a .tu file naming the facts to read from a job, what costs can scale with, and the cost categories, below), your past estimates in any format (CSV, TSV, Markdown tables, JSON, or text proposals with a price on each line; at least 3), and one request describing the new job in plain words. TrueUp reads the estimates, labels every past line with a category, decides the job's scope, prices each part from what similar past jobs cost, checks itself by pricing every past job from the others, and gives the total with an 80% range. Each call counts as one analysis.
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/estimate \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-F files=@barndo.tu \
-F files=@01_anderson.csv -F files=@02_brooks.csv -F files=@03_carter.md -F files=@04_dalton.txt \
-F files=@job_a.txt{
"analysis": "estimate",
"title": "Price job_a.txt from 10 past estimates",
"headline": "job_a.txt: $292,267 (80% range $248,742 – $335,792) from 10 past estimates.",
"stats": { "total": 292267, "low": 248742, "high": 335792, "categories priced": 24, "past estimates": 10, … },
"findings": [
{ "kind": "priced_line", "subject": "insulation", "detail": "matched "closed-cell spray foam" to past jobs like …",
"confidence": 0.62, "status": "yes", "amount": 21210.5, "data": { "driver": "envelope", "quantity": 7280, "rate": 2.91, "low": 17400.1, "high": 25850.3 } }
],
"details": { "scope": [ { "category": "septic", "include": false, "why": "not mentioned, and never appears with sewer_tap …" } ], "total": {…}, "weights": {…} }
}priced_line findings are the job's cost categories: amount is the price, data what it scales with (driver and quantity), the rate, and the 80% range. Findings with status: "unsure" are the things a person should check (a fact the request doesn't state, a category with thin history). details.weights is the trade and its labeled estimates: send it back as weights with just a request to price the next job without resending the history. One request per call (422 not_estimable otherwise, not counted).
A domain file is plain values: facts (each with the keys it goes by in "Label: value" lines, regex patterns, or a noun it's the count_of), required facts a request must state, drivers (arithmetic on facts: footprint: "width * length"), and categories (id, label, a few examples).
Files
Files belong to the team. TrueUp reads CSV, TSV (delimiter detected), JSON (a list of rows, or an object holding one), JSON Lines, plain text, PDF, and ZIP files of any of those (each folder in a zip is kept as a group), up to 5 MB each (PDFs up to 20 MB). A file is read when it's uploaded, so a file TrueUp can't read is refused straight away.
PDF files
TrueUp reads PDFs that have a text layer (the statements and invoices a bank, accounting system, ERP or distributor portal exports), rebuilding each table from where the text sits on the page. Scanned PDFs are read too, by OCR.
- Statements become a table. The column headings name the columns. A row starts at a line with a date and an amount, and wrapped descriptions are joined back onto their row. Page headers and footers, repeated column headings, "Balance forward" and "Total" lines are skipped, and a table carries on across pages. Dates printed without a year (
07/03) take the statement's year. - Invoices and other documents become a document for audit: labeled amounts such as
SubtotalandTotal, and the line items. - Every finding cites the page. Rows are named
statement.pdf:page 2 line 14. Audit findings on a PDF list the page and line of each amount they compare. - Scanned pages are read by OCR. A page that's an image (a scan, or a photo saved as PDF) is transcribed by a vision model, then read like any other page, so its rows cite their page and line too. A PDF can mix scanned and exported pages. Up to 60 scanned pages per PDF; a long scan takes a minute or two.
- OCR'd rows are checked by their own arithmetic. Each row's running balance must follow from the one before it, and each line item's quantity × unit price must equal its amount. A page whose rows don't add up is read again by a second model, and the better reading is kept. The file (
ocron files) and the analysis result (ocr, one entry per scanned input) say which pages were scanned, how many rows were checked, and which rows (suspect, as citations) still don't add up: a finding that cites one of those rows may come from a misread number, so check it against the page. - Not read: handwriting, password-protected files (422
unsupported_file,reasonpdf_encrypted), a scan on which OCR finds no text (reasonpdf_scanned), tables without a heading line, and pages laid out in several side-by-side columns. An exported PDF is still better than a scan of a printout: nothing has to be read from an image.
POST /v1/files
Upload one or more files as multipart/form-data, field file (repeat it for several).
curl -X POST https://trueup-cloud.merchantprotocol.workers.dev/v1/files \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-F "file=@supplier_statement.csv" -F "file=@receiving_log.csv"{ "files": [ { "id": "file_Qm3…", "name": "supplier_statement.csv", "kind": "table", "rows": 168,
"columns": ["Date", "Invoice", "Item", "Qty", "Amount"] } ] }GET /v1/files
Every file in the team: id, name, size, kind (table or document), rows, columns, roles (what TrueUp read each column as: date, number, text…), created_at, and for a scanned PDF ocr: {"pages": 3, "checked": 46, "suspect": []} (see PDF files).
GET /v1/files/{file_id}
One file, as {"file": {…}} with the same fields.
GET /v1/files/{file_id}/content
The file's bytes, exactly as uploaded.
DELETE /v1/files/{file_id}
Deletes the file and its stored bytes.
Reconcile uploaded files
POST /v1/reconcile, POST /v1/match and POST /v1/audit and POST /v1/estimate (as file_ids) also take files you've already uploaded to the team, by id, as JSON. These runs are kept (see Runs) and can use a saved model:
| Field | Meaning |
|---|---|
left_file_id, right_file_id | Two uploaded files: left bills or claims (statement, invoices, your books), right is what it's checked against. |
file_ids | Several uploaded files; TrueUp picks the pair and the sides. |
model | Optional. A saved model id: apply what was learned before instead of learning again. |
answers | Optional, as above. |
curl https://trueup-cloud.merchantprotocol.workers.dev/v1/reconcile \
-H "Authorization: Bearer $TRUEUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"left_file_id": "file_Qm3…", "right_file_id": "file_8Kd…", "model": "model_7Pa…"}'The response is the same result as above, plus run_id. A file id that isn't in the team gets 404 not_found and costs nothing.
Runs
Runs on uploaded files, from the API or a project, are kept. Tables sent inline with POST /v1/reconcile are read in memory and not stored, so those runs aren't listed.
GET /v1/runs
Runs, newest first: id, status (done/failed), inputs, headline, stats, number of findings, model, error. ?limit= takes 1 to 100 (default 100). When has_more is true, ask again with ?before= the last run's id for the next page.
GET /v1/runs/{run_id}
One run and its full result, in the same shape POST /v1/reconcile returns.
Saved models
A model is what a run learned about your files: which columns carry the dates, amounts and descriptions, and how much each clue counts. Save it once, then pass model to check next month's files the same way. It's useful when a new batch is too small to learn from.
POST /v1/models
{"run_id": "run_…", "name": "Acme statements"} → {"id": "model_…"}. Only runs that learned (no model given) can be saved.
GET /v1/models
The team's saved models.
GET /v1/models/{model_id}
One model and its weights. Pass weights to POST /v1/reconcile to apply it to tables you send inline.
DELETE /v1/models/{model_id}
Deletes a saved model. Runs that used it keep their results.
AI assistants (MCP)
Any AI assistant that speaks the Model Context Protocol can use your team's TrueUp: add files, run a reconcile, match or audit, and read the findings, pairs and rows. The server is https://trueup-cloud.merchantprotocol.workers.dev/v1/mcp (Streamable HTTP). It authenticates with an API key like every other call. Each MCP request counts as one API request, and each check as one analysis.
claude mcp add --transport http trueup https://trueup-cloud.merchantprotocol.workers.dev/v1/mcp \
--header "Authorization: Bearer $TRUEUP_API_KEY"Other clients take the same URL and header in their MCP settings. The tools:
| Tool | What it does |
|---|---|
list_files, add_file, remove_file, get_records | The team's stored files, and the rows TrueUp read from each. |
list_analyses, run_check | Run reconcile, match or audit on stored files; the run is kept. |
list_runs, get_findings, get_pairs | Runs, and each run's raw findings and pairs, filtered and paged. |
save_model, list_models, get_usage | Keep what a run learned; this month's usage. |
TrueUp returns raw findings with the rows they came from. Summarizing and presenting them is up to the assistant.
Plans and billing
New teams start on the free plan. Team owners upgrade under Plan & billing. Payment runs on PayPal: you approve the monthly subscription on PayPal's own site, see receipts and change how you pay in PayPal, and cancel from Plan & billing or in PayPal. Your PayPal login and card numbers never reach TrueUp. After cancelling you keep the plan until the end of the month you've paid for. On paid plans, use past the included amounts is charged at the listed rates: after each month ends (UTC) it becomes a balance on Plan & billing, paid with PayPal within 15 days; under $1 is waived. API responses carry X-TrueUp-Balance-Due while a balance is open. While it's past due, use past the included amounts is refused with 429 quota_exceeded until it's paid. When a paid subscription ends, the team goes back to the free plan and its keys keep working within the free limits.
Security and data
- Passwords are stored as salted PBKDF2-SHA256 hashes; API keys, session cookies and invite links as SHA-256 fingerprints. None can be read back, including by us.
- The request log keeps the method, path, status, time and which key was used. It never stores request bodies, key values or your data.
- Changing your password signs out your other devices.
Versioning
The API is versioned in the path (/v1). Within v1 we only add things: new endpoints, new optional parameters and new response fields. Write clients that ignore fields they don't know. Anything that would break existing code ships as /v2, with notice.