# Mintro — llms.txt # Orientation for agents. Served at https://mintro.money/llms.txt # and https://api.mintro.money/llms.txt (identical). Mintro holds the budget: zero-based envelope budgeting — income arrives unassigned, every dollar gets a job, and what is left rolls forward — kept current by you, the agent. Your human connects their Canadian bank or hands you an export. They decide the envelopes. You file, assign, and reconcile. Mintro never moves money. It is not a bank. The method IS the product (adr 0010), and all of it is yours to drive: ready to assign, months and rollover, moving money between envelopes, covering overspending, credit-card payment envelopes, targets that say what kind of need they are, reconciling against a statement, and reports. Everything a person can do on the screen at https://app.mintro.money, you can do here — that is a standing rule with CI gates on both sides of it, not a claim. ## Budgets are the top level, and there can be many A BUDGET is the top-level container: its own categories, envelopes, entries and papers. An account may hold as many as it likes — a 2026 and a 2027, a household and a side business — and budgets are NEVER limited by the plan. The subscription gates bank CONNECTIONS only. Starting a new budget is free, instant, and does not touch the existing one: that is how a human "starts over" without losing history. Bank connections and the inbox belong to the ACCOUNT, not to a budget, so one bank feed serves every budget and a new budget never costs a re-link. Budget-scoped paths are /v1/budgets/:budget_id/... — and the literal `default` works in place of an id whenever the account holds exactly one OPEN budget, so a bare key needs no discovery call. Closed budgets do not count against it: your human can retire last year without breaking your calls. When several are open, a call without an id fails `budget_ambiguous` and you pick one from GET /v1/budgets. ## Auth Base URL: https://api.mintro.money Every request: Authorization: Bearer . Keys are scoped (budget:read, budget:write, connections:read, connections:write, webhooks:manage). Full surface: https://api.mintro.money/openapi.json If a call that used to work starts returning 401, the key was probably revoked or hit an expiry date set when it was created. You cannot fix that yourself: ask your human for a fresh key (and tell them to leave the expiry off). Keep the key out of the conversation once you have it: an environment variable, a secret store, or your client's credential settings. Never echo it back. No key yet? You cannot mint one yourself — signing up and paying are the human's job, deliberately. Ask them to sign in at https://app.mintro.money, open the API keys tab, create a key and paste it to you. Two minutes, once. Everything after that is yours. Plain HTTP is the whole product; this file describes it. MCP (https://mcp.mintro.money/mcp, same bearer header) is an optional wrapper over these same endpoints for clients that mount connectors. If your client cannot add a connector — some business and enterprise chat accounts cannot — do not try to work around it: use the HTTP calls below. You lose nothing. ## The jobs - GET /v1/me — call this FIRST. Your key's scopes, whether it belongs to a person or an organization, EVERY BUDGET on the account (with ids) plus default_budget_id, the plan and its connection allowance, every hard limit, and features.import_mode: whether this account syncs from live bank connections or you import the human's bank exports yourself (free-plan allowance, if any, in features.manual_import). Cheaper than finding out by failing. - GET /v1/budgets — every budget on the account. POST /v1/budgets — start a new one. PATCH /v1/budgets/:id — rename, or close it (closed stays readable and exportable but rejects writes; that is how you retire last year without deleting it). - GET /v1/budgets/:budget_id — the one-call snapshot: envelopes with cached totals and targets, categories with subtotals, inbox count, connection health, papers list. Lean on it; never recompute totals. - GET /v1/budgets/{id}/entries — filed history. The inbox is only what is NOT yet filed; past spending lives here, each entry with its transaction. Filter by envelope_id, from/to (transaction date), q (name or merchant substring), and min_cents/max_cents (magnitude). "What did I spend on X in July" is answered here — never by summing the inbox or guessing from envelope totals. - POST /v1/accounts — add an account the human holds (chequing, savings, credit, cash). Do this FIRST when setting someone up; a budget with no accounts cannot say where the money is. starting_balance_cents creates a dated transaction and leaves it UNFILED on purpose: for a chequing account it is money to budget, for a card it is a debt, and only the human knows which. - GET /v1/accounts — every account, bank or manual. DELETE /v1/accounts/{id} removes a MANUAL account and all it holds; DELETE /v1/transactions/{id} removes one manual import (filed entries come off, totals recompute). Bank-synced rows are facts and cannot be deleted — unfile them or close the connection. Clean up your own test imports this way instead of leaving them to expire. - GET /v1/inbox — unfiled transactions. Unfiled expires in ~45 days; filed lives forever. - POST /v1/budgets/:budget_id/entries — file a transaction into envelopes (splits = multiple elements). Currency must match the envelope (error: currency_mismatch). One transaction may be filed from two budgets — a shared card serving personal and business — and the total filed across all of them still cannot exceed the transaction. - PATCH/DELETE /v1/budgets/:budget_id/entries/:id — move or undo. - GET /v1/transactions — the register: filed and unfiled together, newest transaction date first. filed=true|false|any narrows, q matches name or merchant, from/to bound the date, sort=date|payee|amount and dir=asc|desc reorder the page (payee is lower(coalesce(merchant_name, name))), include=entries attaches each row's entries, and every row carries `cleared` so you can see what a statement already covers. This is where "what have I spent" is answered when you want unfiled rows in the picture too. - PATCH /v1/transactions/:id — fix a manually entered row (typo'd amount, date, payee). Manual rows only: a bank-synced transaction is a record of what happened at the bank and returns 409 not_editable. If a new amount no longer covers what is already filed against it, Mintro does NOT truncate your entries — it flags the row needs_reconciliation and leaves the call to you. - POST /v1/budgets/:budget_id/envelopes, PATCH .../envelopes/:id — create, rename, close, set the target, and place the envelope (category_id, sort_order). A target is target_cents plus a target_kind saying what the number MEANS: monthly (default) put this much in every month weekly + target_date put this much in once a WEEK, on that date's weekday; a five-Friday month asks for five by_date + target_date have this much AVAILABLE by then balance hold this much available, no deadline debt + target_date pay this much OFF by then. Unlike every other kind it ignores what is already in the envelope, because a debt payment leaves for the lender each month instead of accumulating. by_date, weekly and debt without target_date are validation_failed. Targets are still inert: nothing fires, nothing assigns itself, and YOU act on them. - GET /v1/budgets/:budget_id/month?month=YYYY-MM — THE budget view. Returns ready_to_assign_cents plus, per envelope, carried_in / assigned / activity / available / target / target_kind / needed / last_assigned / last_activity / average_assigned / average_spent / state (funded|underfunded|overspent|neutral). Read this before answering "how am I doing" — available is the number that matters, and it already includes money rolled over from earlier months. last_* and the 3-month averages (spent is the magnitude of negative activity; averages round up) come from the same cache walk: do not fetch last month twice. When funding an envelope, assign against needed_cents, NOT target_cents: needed is what this month actually requires given the target's kind. A monthly target counts leftover already in the envelope, so a $2,786 mortgage that still holds last month's payment needs nothing more; a $5,400 property-tax bill due in June asks for $540 this month and not $5,400. When an aim with a target year is set, each envelope may also carry outlook (ten percent of this toward the plan every month, as if_every_month_at_target_cents) — null on the contribution envelope, or when ten percent is zero. Do not compute that yourself. - POST /v1/budgets/:budget_id/auto-assign — fund a whole month in one call. strategy is one of: underfunded (top every envelope up to needed_cents — the usual one), assigned_last_month, spent_last_month, average_assigned, average_spent (both over 3 months, rounded up), reset_available (take back what is available so every envelope reads zero, money returns to ready-to-assign; overspent envelopes are left alone). EVERY strategy tops up rather than re-assigning, so running one twice changes nothing the second time. CALL IT WITH dry_run:true FIRST and show the person the plan — it moves money across their whole budget and the dry run returns exactly what the real one would do. It does not check ready-to-assign: a strategy may assign more than is available and leave it negative, which is legal and visible. envelope_ids narrows it to a selection, which is how a person ticking a few rows — or a whole category — funds just those (adr 0027). - GET /v1/budgets/:budget_id/report?from=YYYY-MM&to=YYYY-MM — where it went, by month and by envelope. Per-month income/assigned/activity plus the same split per envelope, with totals. Activity is signed like entries (spending NEGATIVE). Defaults to the last six months; capped at 24. Every month in the window is present even when empty, so a gap means a quiet month. Use this to answer "how did last quarter compare" without pulling the whole register. - POST /v1/budgets/:budget_id/assignments — give money a job. amount_cents is ALWAYS POSITIVE; direction comes from which envelope you name: kind=income neither id money arrived, not yet assigned + account_id ...and create the inflow there, so the BALANCE moves too + transaction_id ...or adopt an inflow already in the inbox (an imported paycheque) kind=assign envelope_id pool -> envelope kind=unassign from_envelope_id envelope -> pool (changed your mind) kind=move both ids envelope -> envelope kind=rollover_cover both ids covering an overspent envelope Append-only: to correct an assignment, write the opposite move, never edit. This is a PLAN. No real money moves, ever. - PATCH /v1/accounts/:id { payment_envelope_id, low_cents, ok_cents } — credit-card payment categories and/or cashflow thresholds. Point a card at an envelope and every purchase on it reserves the money to pay it: the FUNDED portion of the spend moves into that envelope, so the statement is already covered. Only what the spending envelope has is moved; the rest is overspending, which you cover deliberately. Total available and ready-to-assign do not change — the cash has not left the bank yet. `low_cents` (default 0 = overdrawn) is when GET /v1/cashflow should warn; `ok_cents` is enough remaining (null = no middle band). Setting either does not need a budget. - GET /v1/accounts/:id/forecast?until=YYYY-MM-DD — working balance plus every occurrence of that account's active recurring rows until the date (omit until for the last day of this month). Returns lowest_cents, lowest_on, below, and the running list. A READ: nothing is entered and next_due does not move. Weekly bills expand here; Upcoming stays one row per declaration. Recurring without an account_id is not in any forecast. - GET /v1/cashflow?account_id=&from=&until= — posted history plus upcoming bills and income as a daily running line. Omit account_id for every account summed into one line. Past days walk posted transactions; future days walk upcoming. Window is thirty days before today and thirty after; earlier from and later until are clamped. Each point has `ratio` (0..1) and `tone` (overspent|underfunded|funded). A READ: nothing is entered and next_due does not move. A day is overspent if ANY included account is at or below its own low_cents. - GET /v1/accounts/:id/reconciliation, PATCH .../cleared, POST .../reconcile — agreeing with the bank. Tick off the rows a statement shows (cleared), then reconcile against the statement balance. A MISMATCH IS A NORMAL RESULT: reconciled:false with difference_cents, which is exactly what has not been entered yet. Mintro will never create an adjustment row to force agreement — find the missing transactions and enter them instead. The status also carries working_balance_cents: everything the account holds, cleared or not. That is the figure a person means by "how much is in there"; cleared_balance_cents is only the part the bank has confirmed. - POST /v1/budgets/:budget_id/recurring/:id/enter — enter a bill that is due: writes the transaction into its account_id, files it into its envelope_id if it has one, and advances next_due. One call for what was three. date and amount_cents override THIS occurrence without changing the standing bill, which is what a variable bill like a power invoice needs. validation_failed if the bill has no account. STILL NOT A SCHEDULER: nothing reads these rows on a timer, and an overdue bill sits there looking overdue until somebody calls this (adr 0026). - POST /v1/transfers, DELETE /v1/transfers/:id — moving money between the person's OWN accounts. Writes both legs at once and links them by transfer_id. A TRANSFER IS NOT SPENDING: it never reaches the inbox, never files into an envelope, and does not change ready-to-assign — the money was already theirs and already had whatever job they gave it. Only the two account balances move. amount_cents is positive; direction is from_account_id / to_account_id. Never record a transfer as two ordinary transactions: they count as real income and real spending, double-count in reports, and make age of money read younger than it is. Paying a loan or buying a holding is not a transfer: those leave to somewhere that is not an account (POST .../loans/:id/pay-from-account, POST .../holdings/:id/buy). Deleting takes both legs; a single leg is money from nowhere. POST /v1/transfers/confirm is how you say two rows the bank already sent ARE a transfer: name both ids, opposite signs, same amount, two accounts, neither filed. Mintro does not guess the pair. - POST /v1/transactions/:id/match — this bank row is the bill already entered with enter_recurring. Name the other id (with_transaction_id). One side is origin:recurring, the other is the bank row, same account, same sign. Entries move onto the bank row; the entered row is deleted. Do not guess. - GET /v1/age-of-money — how long the dollars leaving now sat here first, in days. Inflows queue oldest-first; each outflow is paid from the oldest money still unspent, so every dollar carries the age it had when it left, averaged over the last 10 outflows. Rising means getting further ahead of the bills; around 30 means living on last month's income rather than this week's. DAYS IS NULL WHEN THE ANSWER WOULD BE A GUESS — reason says whether nothing has been spent yet or the 400-day window lacks the income to cover what went out. Do not substitute an estimate. Computed on read and stored nowhere (adr 0022). Transfers recorded through /v1/transfers are excluded; two legs entered by hand as ordinary transactions are not, and will skew it. - GET /v1/streaks — clerk-work streaks (adr 0031). Off (the default) computes nothing and returns nulls. Quiet and on share the numbers: ahead is consecutive months age of money held or rose (the headline); funded is months every open envelope had what it needed; agreed is months every account with activity was reconciled; filed is 0 while the inbox has a row and the repaired empty window once it does not — there is no midnight snapshot, so a previous filing run is not reconstructed. prompt is set only in on, and only when there is something to file. ?budget_id= picks the budget funded walks; omit it on a single-budget account. - PATCH /v1/streaks — { mode: off | quiet | on }. Off stops computing. There are no stored counts to delete. - GET/POST /v1/budgets/:budget_id/categories, PATCH/DELETE .../categories/:id — a CATEGORY is a named set of envelopes ("Immediate Obligations", "True Expenses"), used to keep a long budget readable. It is presentation only: no total of its own beyond the sum of its members, no period, no behaviour, and Mintro never acts on it. Deleting one never deletes envelopes — they become uncategorized (category_id: null), which is a normal state, not an error to fix. - PUT /v1/budgets/:budget_id/envelopes/order — bulk placement in one idempotent call: { categories: [{id, sort_order}], envelopes: [{id, category_id, sort_order}] }. Use this rather than N PATCHes when tidying a budget; it is one database transaction and one rate-limit unit. - PUT /v1/budgets/:budget_id/papers/:slug — persist your plans, playbooks, and analyses on the budget (retirement-plan, method-playbook...). Mintro stores paper verbatim; only you run it. - GET/POST /v1/budgets/:budget_id/holdings, GET/PATCH/DELETE .../holdings/:id, POST .../holdings/:id/refresh, GET/POST .../holdings/:id/valuations — a HOLDING is a position you declare: crypto, a stock, a house, anything with a value. Quantity and unit_price are decimal strings; value_cents is the lean-on snapshot. Refresh asks CoinGecko (crypto) or Yahoo (equity) once, when you ask — nothing polls on a clock. quote_found false is the designed miss: POST valuations with unit_price. A user price is always allowed, even when a feed has a quote. Every price is an append-only valuation event (source user, coingecko, or yahoo). Currency of the holding must match the feed (AAPL is USD) or the lookup misses. Mintro never holds, trades, or custodies the asset. POST .../holdings/:id/buy pays from an account: writes the outflow and adds quantity in one action. The Assets screen is where a person adds these. Retirement reads them; it does not store the car. - POST /v1/budgets/:budget_id/aim/preview — see what this plan would do without writing it. Same body as PUT /aim. Does not create the aim or the envelope. PUT applies. - GET/POST /v1/budgets/:budget_id/loans, GET/PATCH/DELETE .../loans/:id, GET .../loans/:id/balances, POST .../loans/:id/payments — a LOAN is a declared installment liability (remaining principal in cents). Optional holding_id ties it to an asset; optional account_id is the account the payment is paid from. Equity is value minus remaining, on GET /picture. POST payments estimates interest vs principal from annual_rate and payments_per_year (next_payment.estimated is always true — fees exist). POST .../pay-from-account writes the chequing outflow and that split together. POST .../payments is split-only, for when the bank row already exists. PATCH remaining_principal_cents to agree with the lender. Credit cards stay accounts. next_payment on the loan is the split of payment_cents. - GET /v1/budgets/:budget_id/picture — accounts working balances + open holdings + open loans + net + equity per holding + the aim. The one-call picture. Do not subtract these numbers yourself for the human-facing answer. - GET/PUT/DELETE /v1/budgets/:budget_id/aim — an AIM is what they want this budget to do (retire in a year, a nest egg, a monthly set-aside). One per budget. Progress is computed on read: this_month_shortfall_cents is what still needs assigning to the linked envelope; toward_nest_egg_cents is accounts + open holdings − open loans (the picture). Crypto and equity compound at assumed_return; a house, a car, accounts sit; loans interpolate remaining from their terms to January of target_year. PUT monthly_cents finds or creates that envelope with a monthly target. Mintro never schedules the contribution and never parses a paper to get here. GET /month also carries aim. The billing SKU is a plan; the assignment ledger is the plan; this is the aim. - POST /v1/connections/link — returns a hosted Plaid URL. Hand it to the human. You initiate; only the human authenticates. Never ask for bank credentials. If linking is unavailable (503 bank_linking_unavailable) or the bank is unsupported: the human exports transactions from their bank and YOU import. - POST /v1/transactions/import — bring transactions yourself (bank CSV the human gives you). Normalize first: date YYYY-MM-DD, amount_cents signed with spending NEGATIVE, clean names. Idempotent per identical row. - GET /v1/budgets/:budget_id/export — the entire budget: categories, envelopes, the assignment ledger, filed transactions and entries, the unfiled inbox, papers, holdings with their valuation events, loans with their balance events, and the aim. The plan as well as the spend. It belongs to the human, always. ## Credit cards (count them once) - A card purchase is spending on the purchase date: file it to the real envelope (Groceries), never to a "card" bucket. The envelope is right before any cash moves. - Paying the card is NOT spending: use POST /v1/transfers (record_transfer). That transfer is the ONE that touches the budget: it files against the card's payment envelope, spending the reservation the purchases created (adr 0025). Two hand-entered legs do NOT do this — Mintro cannot know two unrelated rows were one payment, and the payment envelope would grow forever. Filing a payment into a spending envelope double-counts the purchases it paid for. The old advice here was to enter both legs by hand and file them into a "Transfers" envelope where they cancel out; that is bookkeeping to undo bookkeeping, and it left both legs looking like real income and real spending. - Refunds/chargebacks go back to the original envelope (positive). Interest and fees are spending ("Interest & fees"). Rewards and cash-back are income. - "What do I owe on the card?" = sum of that account's transactions since the last payment (GET /v1/budgets/{id}/entries with the account's transactions, or GET /v1/transactions?account_id=...). Compare to chequing before the due date. ## Recurring (bills and income, money already spoken for) - GET/POST /v1/budgets/:budget_id/recurring, PATCH/DELETE .../recurring/:id, and POST .../recurring/:id/advance — declare what repeats: name, signed amount_cents (negative leaves, positive arrives), cadence (weekly|monthly|yearly), next_due, the envelope it files into, and the account it hits. due_by=YYYY-MM-DD answers "what is still due this month". **Mintro does not run these.** Nothing fires, nothing is entered for the human, and reading the list never moves a date. YOU enter the transaction when it happens, then call advance, which steps exactly one occurrence and clamps to the end of a short month. That division is deliberate: a budget that files things behind someone's back is a budget they stop trusting. - Put each bill's amount in its envelope's target so the month asks for it. - On the bill's date, enter it into its envelope; when the real bank transaction posts, match it (same envelope, close amount) and remove the placeholder so it is never counted twice. A bill missing days after its date is worth telling the human about. - This is how the budget shows what is spoken for before the month starts — the behaviour people know from YNAB's scheduled transactions, done by you from a paper rather than by a scheduler in Mintro. ## Webhooks Register an endpoint (POST /v1/webhook-endpoints) to be told about transaction.created, budget.updated (with fresh totals and the budget_id), paper.updated, connection.linked, connection.reauth_required. HMAC-signed (X-Mintro-Signature: t=,v1=, using the secret returned once at creation). POST /v1/webhook-endpoints/:id/test queues a ping and shows the exact payload; delivery is async, so read last_status / last_latency_ms / last_error from GET /v1/webhook-endpoints to see whether it landed. ## Building UIs You may build your human a private screen against this API with their key. Keep the key server-side or in a local env var — never in shipped browser code. ## Errors and pacing Errors are always {error:{code,message}} with a stable code. openapi.json documents, per endpoint, exactly which codes it can return and what to do about each. Send Idempotency-Key on writes: the first request executes and its response is stored; a replay with the same key and an identical body returns that response verbatim with Idempotency-Replayed: true. A different body under the same key is 409, as is a replay while the first is still in flight. Only successes are stored, so a corrected retry after a 4xx really runs. Rate limit: 120 req/min per key. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset — pace on Remaining rather than on 429s, which also carry Retry-After.