API · 16 operations

Transactions

The bank's rows: inbox, import, edits, transfers, matches.

get /v1/inbox

Unfiled, unexpired transactions, newest first

scope budget:read · getInbox

Parameters

  • budget_id query · string

  • cursor query · string

  • limit query · integer · ≥ 1 · ≤ 100

Returns · 200

{ transactions[], next_cursor }. Unfiled transactions expire after ~45 days.

Example

curl 'https://api.mintro.money/v1/inbox' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

get /v1/transactions

Stored transactions: the register

scope budget:read · listTransactions

Parameters

  • budget_id query · string

  • account_id query · string

  • loan_id query · string

  • holding_id query · string

  • envelope_id query · string

  • since query · string

  • from query · string

  • to query · string

  • q query · string

  • filed query · string

  • cleared query · string

  • include query · string

  • sort query · string

  • dir query · string

  • cursor query · string

  • limit query · integer · ≥ 1 · ≤ 100

Returns · 200

{ transactions[], next_cursor }: filed and unfiled together, only what Mintro stores. Default order is transaction date descending (a register, not an insert log); sort=date|payee|amount and dir=asc|desc change that. payee sorts lower(coalesce(merchant_name, name)). Cursor is keyset on the active sort plus id. filed=true|false|any narrows (default any); cleared=true|false|any narrows on the bank side the same way (a reconciled row is cleared); q matches anything about the row: the bank's own words (name, merchant, category, also on the bank row it landed from), the payee, the note, any envelope it is filed into and any part's note (every part of a split), the account, the loan or holding it pays, the date (2026-10), and the amount: 2310 or $2,310 finds $2,310.00–$2,310.99 either sign, 2310.50 exactly; from/to bound the transaction date; include=entries attaches each row's entries in one extra read, and unfiled_cents, the part no entry holds (signed like the row). A row with two or more entries, or one entry and a non-zero unfiled_cents, is a split: its parts are the entries plus that remainder, which is ready to assign when ready_to_assign is true and otherwise filed nowhere. Every row carries cleared, so you can see what a statement has already accounted for without guessing (pair it with account_id to work an account down against its statement; a row the feed or an import delivered is cleared on arrival, a row a person typed is uncleared until the bank shows it), and bank_withdrawn_at: set, the bank took this posted row back and sent nothing in its place. It stays in the budget, money and all, until the person keeps it or deletes it (a withdrawn row is no longer the bank's word, so DELETE works on it); a later re-send of the same payment takes the row over and clears the mark. And reconciled_at: set, the row was locked by a reconciliation and its date, amount, account and cleared mark cannot change until POST /v1/transactions/{id}/unlock; its envelope always can. A cleared row never expires, filed or not. Every row carries note, the memo a person or agent put on it (PATCH sets it on any row). A transfer leg carries transfer_id: delete the pair, never one side. It also carries transfer_account_id and transfer_account_name, the other leg's account, so a register can say Transfer : Savings; both are null on any other row. A transfer leg is never in the inbox. ready_to_assign is true when an income row counts the transaction in ready to assign: an inflow that landed, a cash account's starting balance (negative when it opened overdrawn). A card's negative starting balance is settled with neither an envelope nor income: debt from before Mintro, paid down from the card's payment envelope. origin is bank | recurring | import | transfer | income | opening | loan_payment | holding_buy: recurring is a bill you entered, and that is what POST /v1/transactions/{id}/match absorbs into a later bank arrival. A row that paid a loan carries loan_id and is settled by that link; loan_id= narrows to one loan's payments; a row that bought or sold a holding carries holding_id and holding_id= narrows the same way. envelope_id= narrows to rows with a part in that envelope (a split counts when any part is in it); envelope_id=none to rows with money in no envelope yet: nothing filed, or a split's remainder that is not ready to assign (transfers and counted income are never in it).

Example

curl 'https://api.mintro.money/v1/transactions' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

post /v1/transactions/import

Manual import: the agent brings normalized transactions (spending negative); idempotent per identical row

scope budget:write · importTransactions

Parameters

  • budget_id query · string

Body · JSON

  • account string · ≤ 100 chars

  • budget_id string · ≤ 64 chars

  • transactions object[] · required · ≤ 1000 items

  • transactions[].date string · required · YYYY-MM-DD

  • transactions[].amount_cents integer · required · ≥ -9007199254740991

  • transactions[].name string · required · ≤ 300 chars

  • transactions[].merchant_name string | null · ≤ 300 chars

  • transactions[].currency string · matches ^[A-Z]{3}$ · ≥ 3 chars · ≤ 3 chars

  • transactions[].external_id string · ≤ 200 chars

  • transactions[].cleared boolean

  • transactions[].note string | null · ≤ 1000 chars

Returns · 201

{ imported, transaction_ids[], skipped, skipped_rows[] }. skipped_rows names the rows that collided with an existing transaction, so a re-import of the same export is safe and legible. Outflows land in the inbox; inflows are ready to assign on arrival in the tenant's one open budget (or budget_id), so do not record_income for them again. A row that is plainly a declared bill is adopted on arrival: same account, same sign, due within three days or already entered as a placeholder, and either the exact cents, or (with no exact candidate) the bill's name on the row and the amount within 20% of the bill; exactly one candidate, never a tie. It is filed or ready to assign at the bank's amount, linked (recurring_id, entered_via: auto_match), the bill advanced, its own amount unchanged; adopted[] lists them. Softer matches wait in the inbox with a suggestion (GET .../recurring/matches). Rows arrive cleared because the bank delivered them; cleared: false on a row marks one the person told you about that the bank has not shown yet, and it stays uncleared until a person or the bank clears it.

Example

curl -X POST 'https://api.mintro.money/v1/transactions/import' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "account": "Chequing", "transactions": [ { "date": "2026-09-12", "amount_cents": -8412, "name": "COSTCO #512", "external_id": "FIT-88213" }, { "date": "2026-09-15", "amount_cents": 212500, "name": "PAYROLL ACME INC" } ] }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 402

    plan_limit: The tenant's plan does not allow this. Read entitlements on GET /v1/me to see what the plan unlocks and allows, report the limit to the human, and let them choose to upgrade at https://mintro.money/pricing; do not retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

post /v1/transactions/import/csv

Import a bank export as written: Mintro reads the CSV (YNAB's register, RBC, Chase, Amex, TD/CIBC without a header, or any headed file) and converts to cents; dry_run previews

scope budget:write · importCsv

Parameters

  • budget_id query · string

Body · JSON

  • csv string · required · ≤ 1000000 chars

  • dry_run boolean

  • account string · ≤ 100 chars

  • account_id string · ≤ 64 chars

  • account_from_file boolean

  • budget_id string · ≤ 64 chars

  • columns object

  • columns.date integer · required · ≥ 0 · ≤ 100

  • columns.payee integer · required · ≥ 0 · ≤ 100

  • columns.amount integer · ≥ 0 · ≤ 100

  • columns.outflow integer · ≥ 0 · ≤ 100

  • columns.inflow integer · ≥ 0 · ≤ 100

  • columns.memo integer · ≥ 0 · ≤ 100

  • columns.category integer · ≥ 0 · ≤ 100

  • columns.category_group integer · ≥ 0 · ≤ 100

  • columns.account integer · ≥ 0 · ≤ 100

  • columns.external_id integer · ≥ 0 · ≤ 100

  • columns.cleared integer · ≥ 0 · ≤ 100

  • has_header boolean

  • date_format "ymd" | "mdy" | "dmy"

  • flip_sign boolean

  • file_by_category boolean

  • before_start "skip" | "keep"

Returns · 201

201 on a commit, 200 on a dry run: { dry_run, layout, header, columns, confident, date_format, date_format_ambiguous, flip_sign, sample[][], column_count, row_count, duplicate_count, rows[] (first 200: line, date, amount_cents, name, note, account_name, duplicate, envelope_id), unreadable[] ({ line, reason }), unreadable_count, accounts[] ({ name, account_id|null, rows }), categories[]|null }, and on a commit also imported, duplicate_lines[], transaction_ids[], adopted[] (and filed when file_by_category; transfers, the rows whose payee is Transfer : <account> for another hand-kept account in this budget, landed as a transfer between the two; the other half, in this file or a later one, is skipped as a duplicate). Send dry_run: true first and show the person what it read: layout names a recognised export (ynab | rbc | chase | amex | headed | headerless); confident: false means the columns were guessed from content: confirm or send columns (zero-based indexes: date, payee, amount OR outflow+inflow, memo, category, category_group, account, external_id, cleared), which replaces detection. Dates: year-first and month-name forms read one way; numeric slash dates are month-first unless the file proves otherwise. When it cannot, date_format_ambiguous is true and a commit is refused (400) until you send date_format: mdy|dmy. Amounts accept $, thousands commas, (19.00), CR/DR; a decimal comma or a fraction of a cent is unreadable, never rounded. Amex writes charges positive, so its flip_sign defaults true; send flip_sign to override for any file. Account: account_id (an existing manual account), or account (a name; created if new), or account_from_file: true to send each row to its Account column (YNAB). file_by_category: true files each row whose category names exactly one open envelope in the budget (by name, told apart by category group) through the ordinary filing path; others wait in the inbox. The commit is POST /v1/transactions/import per account underneath: same identity, same re-import safety (a re-import reports every row in duplicate_lines), same free-plan cap, same bill adoption. Up to 5,000 rows and 1 MB per file.

Example

curl -X POST 'https://api.mintro.money/v1/transactions/import/csv' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "csv": "<csv>" }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

    budget_ambiguous: This tenant holds more than one budget, so the call must say which. GET /v1/budgets for the ids, then pass budget_id.

  • 402

    plan_limit: The tenant's plan does not allow this. Read entitlements on GET /v1/me to see what the plan unlocks and allows, report the limit to the human, and let them choose to upgrade at https://mintro.money/pricing; do not retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    budget_closed: The budget is closed. Closed budgets stay readable and exportable but reject writes; reopen it with PATCH /v1/budgets/{id} or write to an open one.

  • 413

    payload_too_large: The request body is over 1 MB (4 MB for an image). Import in batches of at most 1000 rows; send a smaller or downsized image.

patch /v1/transactions/{id}

Edit a manually entered transaction

scope budget:write · updateTransaction

Parameters

  • id path · string · required

  • budget_id query · string

Body · JSON

  • date string · YYYY-MM-DD

  • amount_cents integer · ≥ -9007199254740991

  • name string · ≤ 300 chars

  • merchant_name string | null · ≤ 300 chars

  • note string | null · ≤ 1000 chars

  • payee_id string | null · ≤ 64 chars

  • payee string · ≤ 120 chars

Returns · 200

{ updated, needs_reconciliation, touched_envelope_ids }. Date, amount, name and merchant_name: manual rows only; a bank-synced transaction is a record of what happened at the bank and returns 409 not_editable. payee (a name, made if new) or payee_id (null clears it) is the person's payee and goes on ANY row; the bank's words stay in name. note is different: a memo on the row that goes on ANY transaction, bank-synced or reconciled (it is not a bank fact); a note-only patch never touches an envelope; null clears it. If a new amount no longer covers what is already filed against it, entries are NOT truncated: the row is flagged needs_reconciliation and you decide.

Example

curl -X PATCH 'https://api.mintro.money/v1/transactions/<id>' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "date": "2026-09-15", "amount_cents": 45000 }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    not_editable: Only rows you entered can have their date or amount changed. A row from your bank's feed is a record of what happened at the bank, and the feed would re-sync it anyway. Unfile it if it is misfiled, or correct it at the bank.

    locked: This transaction is reconciled: its date, amount, account and cleared mark were agreed with the bank and are locked. Its envelope can still change. If the bank fact itself is wrong, POST /v1/transactions/{id}/unlock first; the account then shows how far its reconciled balance has moved.

delete /v1/transactions/{id}

Delete a manually imported transaction

scope budget:write · deleteTransaction

Parameters

  • id path · string · required

  • budget_id query · string

Returns · 200

{ deleted: true, entries_deleted }. Any row a person or agent entered can be deleted, on a hand-kept account or on a bank-fed one (a bill entered from recurring, recorded income, a loan payment, an import). Filed entries come off with it, income and loan payments it made are reversed, and envelope totals recompute. A row the bank's feed delivered (or a bank account's starting balance) is a fact: 409 not_deletable (unfile it, or close the connection). PATCH follows the same rule for date and amount_cents (409 not_editable).

Example

curl -X DELETE 'https://api.mintro.money/v1/transactions/<id>' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    not_deletable: A row from your bank's feed, or a bank account, cannot be deleted: it is a fact from the feed. Rows you entered can be. Unfile it (delete its entries) if it is misfiled, or close the connection to stop the feed.

    locked: This transaction is reconciled: its date, amount, account and cleared mark were agreed with the bank and are locked. Its envelope can still change. If the bank fact itself is wrong, POST /v1/transactions/{id}/unlock first; the account then shows how far its reconciled balance has moved.

get /v1/transactions/{id}

Transaction with its entries

scope budget:read · getTransaction

Parameters

  • id path · string · required

  • budget_id query · string

Returns · 200

The transaction plus entries[], which is empty when it is still unfiled.

Example

curl 'https://api.mintro.money/v1/transactions/<id>' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

post /v1/transactions/{id}/match

This bank row is the bill or row I already entered

scope budget:write · matchTransaction

Parameters

  • id path · string · required

  • budget_id query · string

Body · JSON

  • with_transaction_id string · required

Returns · 200

{ bank_transaction_id, deleted_transaction_id, needs_reconciliation }. enter_recurring writes a real transaction so the budget is true on the day you pay; the bank then delivers the same payment. Without this call you have spent it twice. An exact-cent arrival within three days pairs itself on arrival (entered_via: auto_match); for anything softer you name the other id (with_transaction_id), and GET .../recurring/matches ranks the candidates. The bank row inherits the bill link, and an entered paycheque's ready-to-assign row moves onto it so income counts once. One side must be origin: recurring (paired with any arrival) or origin: import (a row typed or imported by hand, paired with a bank row), on the same account with the same sign. Once an account is linked, a bank arrival matching an imported row exactly (same cents, within seven days, closest date) pairs itself on arrival the same way. Entries move onto the bank row through the one write path; the entered row is deleted. The person's payee is kept (the one they set, else the name they gave the row or the bill), and the bank's text never replaces it (it stays in name). The match is remembered as a payee rule (exact bank text, this account, this amount, after your own rules): the next identical arrival lands with that payee, filed where this one was. Amounts that differ flag needs_reconciliation rather than silently resizing entries. Order of the two ids does not matter. Two bank rows for one payment merge here too: banks re-issue rows, and when the bank sent the same payment twice (same account, same cents and words, within three days) the row carrying an envelope, link or reconciliation stays, else the older one; it takes the bank's latest reference and date, and the other goes along with any ready to assign it added. Both filed is refused: unfile one first.

Example

curl -X POST 'https://api.mintro.money/v1/transactions/<id>/match' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "with_transaction_id": "<with_transaction_id>" }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    locked: This transaction is reconciled: its date, amount, account and cleared mark were agreed with the bank and are locked. Its envelope can still change. If the bank fact itself is wrong, POST /v1/transactions/{id}/unlock first; the account then shows how far its reconciled balance has moved.

post /v1/transactions/{id}/unlock

Take one reconciled row out of its lock

scope budget:write · unlockTransaction

Parameters

  • id path · string · required

  • budget_id query · string

Returns · 200

{ transaction_id, account_id }. Clears reconciled_at on the row so its date, amount, account or cleared mark can be corrected, or the row deleted. The reconciliation itself stays on the record; the account's drift_cents on GET /v1/accounts/{id}/reconciliation then says how far its reconciled rows have moved from the statement they were locked against. Deliberate and separate from editing, so nothing changes a reconciled fact by accident: use it only when the bank fact on the row is wrong, and tell the person you did.

Example

curl -X POST 'https://api.mintro.money/v1/transactions/<id>/unlock' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

post /v1/transactions/{id}/loan-payment

This bank row paid a loan

scope budget:write · linkLoanPayment

Parameters

  • id path · string · required

  • budget_id query · string

Body · JSON

  • loan_id string · required · ≤ 64 chars

  • envelope_id string · ≤ 64 chars

  • payment_cents integer · ≥ 1

  • kind "regular" | "extra_principal" | "interest_only" | "fee"

  • interest_cents integer · ≥ 0

  • principal_cents integer · ≥ 0

  • fee_cents integer · ≥ 0

  • note string | null · ≤ 1000 chars

Returns · 201

{ transaction_id, converted, loan, payment }. The row becomes the payment: the loan's remaining principal drops by the principal part, the split is recorded on the loan naming this row, and the row comes out of an envelope, as in YNAB: whatever of it is not filed yet is filed into envelope_id in the same write, so the envelope's activity carries the payment. envelope_id may be left out only when the row is already filed in full; otherwise 400 validation_failed. kind is regular (default: interest and principal from the terms unless you give interest_cents/principal_cents, which must add up with fee_cents to the amount), extra_principal (all to principal), interest_only (nothing to principal), or fee (a fee you paid; nothing to principal). A row in the budget's currency paying a loan kept in another converts at the stored rate for the row's date (converted: true) unless you give payment_cents in the loan's currency. One live payment per row: a second link is 409 already_linked. A transfer leg, an inflow, or a paid-off loan is refused. A row linked before envelopes were required and never filed waits in the inbox for one; it never expires.

Example

curl -X POST 'https://api.mintro.money/v1/transactions/<id>/loan-payment' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "loan_id": "<loan_id>" }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    already_linked: This transaction already pays a loan (or a bank row already pays it). DELETE /v1/transactions/{id}/loan-payment to unlink first; linking twice would drop the principal twice.

    budget_closed: The budget is closed. Closed budgets stay readable and exportable but reject writes; reopen it with PATCH /v1/budgets/{id} or write to an open one.

delete /v1/transactions/{id}/loan-payment

This row did not pay that loan after all

scope budget:write · unlinkLoanPayment

Parameters

  • id path · string · required

  • budget_id query · string

Returns · 200

{ transaction_id, loan }. Appends a reversal on the loan (the history stays append-only) that puts the principal back (unless you agreed a remaining with the lender after the payment, in which case the lender's figure stands and the reversal is history only), clears the link, and, with nothing filed against the row, sends it back to the inbox. Link it again and the payment re-applies.

Example

curl -X DELETE 'https://api.mintro.money/v1/transactions/<id>/loan-payment' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

post /v1/transactions/{id}/holding-trade

This bank row bought into, or sold out of, a holding

scope budget:write · linkHoldingTrade

Parameters

  • id path · string · required

  • budget_id query · string

Body · JSON

  • holding_id string · required · ≤ 64 chars

  • envelope_id string · ≤ 64 chars

  • quantity string · matches ^(0|[1-9]\d*)(\.\d{1,12})?$

  • note string | null · ≤ 1000 chars

Returns · 201

{ transaction_id, kind, holding, trade }. An outflow is a buy, an inflow a sell: the holding's quantity moves by quantity (default the amount ÷ its unit price, so a $100 ETF purchase at $25.40 adds 3.937007874016 units). A buy comes out of an envelope (envelope_id files whatever of the row is unfiled; needed unless it is already filed, else validation_failed), and a sale stays ready to assign. An existing unit price stays; a first buy with no price takes the fill. The row and the holding must share a currency. One live trade per row: a second link is 409 already_linked. A transfer leg, a closed holding, or selling more than is held is refused.

Example

curl -X POST 'https://api.mintro.money/v1/transactions/<id>/holding-trade' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "holding_id": "<holding_id>" }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    already_linked: This transaction already pays a loan (or a bank row already pays it). DELETE /v1/transactions/{id}/loan-payment to unlink first; linking twice would drop the principal twice.

    budget_closed: The budget is closed. Closed budgets stay readable and exportable but reject writes; reopen it with PATCH /v1/budgets/{id} or write to an open one.

  • 422

    currency_mismatch: The envelope's currency differs from the transaction's. Currency is immutable after creation, so file into an envelope of the matching currency or create one.

delete /v1/transactions/{id}/holding-trade

This row did not trade that holding after all

scope budget:write · unlinkHoldingTrade

Parameters

  • id path · string · required

  • budget_id query · string

Returns · 200

{ transaction_id, holding }. Appends a reversal on the holding (the units go back, valued at the current price), clears the link, and with nothing filed against the row sends it back to the inbox; an inflow lands as ready to assign again.

Example

curl -X DELETE 'https://api.mintro.money/v1/transactions/<id>/holding-trade' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

post /v1/transfers

Move money between the person's own accounts

scope budget:write · createTransfer

Parameters

  • budget_id query · string

Body · JSON

  • from_account_id string · required

  • to_account_id string · required

  • amount_cents integer · required · ≥ -9007199254740991

  • budget_id string · ≤ 64 chars

  • date string · required · YYYY-MM-DD

  • note string | null · ≤ 300 chars

Returns · 201

{ transfer_id, from_transaction_id, to_transaction_id, amount_cents, date, payment_envelope_id }. Writes BOTH legs in one database transaction and links them by transfer_id. A transfer is not spending: it never enters the inbox, never files into an envelope, and does not move ready-to-assign, because the money was already yours and already had whatever job you gave it. Only the two account balances change. The one exception is paying a credit card: when the destination is a card with a payment envelope, the outgoing leg is FILED against it, spending the reservation those purchases created, and payment_envelope_id says so. The link is per budget: budget_id in the body names which; with one open budget it is implied; with several open and none named, the leg stays unfiled and payment_envelope_id is null. Two hand-entered legs do not do this and the payment envelope would grow forever. amount_cents is positive; the direction is from_account_id and to_account_id. Do NOT record a transfer as two ordinary transactions: they would count as real income and real spending and would make age of money read younger than it is.

Example

curl -X POST 'https://api.mintro.money/v1/transfers' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "from_account_id": "<from_account_id>", "to_account_id": "<to_account_id>", "amount_cents": 45000, "date": "2026-09-15" }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

post /v1/transfers/confirm

These two existing rows are a transfer (you said so)

scope budget:write · confirmTransfer

Parameters

  • budget_id query · string

Body · JSON

  • transaction_ids any[] · required · ≤ 2 items

  • budget_id string · ≤ 64 chars

Returns · 201

{ transfer_id, from_transaction_id, to_transaction_id, amount_cents, date, payment_envelope_id }. The bank delivers both legs as ordinary unfiled transactions with no link. Mintro does not guess the pair: you name both ids. They must be opposite signs, the same magnitude, two different accounts, neither already a transfer, neither filed. They leave the inbox, and any income the inflow leg landed with on arrival comes back out of ready to assign in the same database transaction. Paying a card still files the outgoing leg.

Example

curl -X POST 'https://api.mintro.money/v1/transfers/confirm' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "transaction_ids": [] }'

Errors · besides 401, 429, 500

  • 400

    validation_failed: The request body or query is malformed. The message names the offending field; fix it and retry.

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

delete /v1/transfers/{id}

Remove both legs of a transfer

scope budget:write · deleteTransfer

Parameters

  • id path · string · required

  • budget_id query · string

Returns · 200

{ deleted }, always 2. The pair is the unit: deleting one side leaves money that arrived from nowhere. Returns 409 not_deletable if either leg has been filed into an envelope; unfile it first, because filing it was a deliberate decision to treat it as spending.

Example

curl -X DELETE 'https://api.mintro.money/v1/transfers/<id>' \ -H "Authorization: Bearer $MINTRO_KEY"

Errors · besides 401, 429, 500

  • 403

    forbidden: The key is valid but lacks the scope this call needs. Ask the human to mint a key with the scope named in the message.

  • 404

    not_found: No such record on this budget. Re-read GET /v1/budgets/{id} (or the relevant list) for current ids rather than reusing a remembered one.

  • 409

    idempotency_conflict: This Idempotency-Key was used for a different request, or the first request is still in flight. Retry the in-flight case after a second; use a fresh key for a genuinely different request.

    not_deletable: A row from your bank's feed, or a bank account, cannot be deleted: it is a fact from the feed. Rows you entered can be. Unfile it (delete its entries) if it is misfiled, or close the connection to stop the feed.