API · 10 operations

Recurring

Bills and income that repeat, and entering them.

post /v1/budgets/{budget_id}/recurring/{id}/enter

Enter a bill: write it, file it, move to its next date

scope budget:write · enterRecurring

Parameters

  • budget_id path · string · required

  • id path · string · required

Body · JSON

  • date string · YYYY-MM-DD

  • amount_cents integer · ≥ -9007199254740991

Returns · 200

{ recurring, transaction_id, filed }. Creates the transaction in the bill's account_id, files it into its envelope_id when it has one, links it as the payment on its loan_id when it has one, and advances next_due: one call for what was three. The bill's note is copied onto the transaction, and survives when a bank row later absorbs it (match or auto-adopt). Income with no envelope (a paycheque, a benefit) is ready to assign on arrival, so filed is true and there is nothing to record_income; only an outflow with no envelope waits in the inbox. date and amount_cents override this occurrence without changing the standing bill, which is what a variable bill like a power invoice needs. Returns 400 validation_failed when the bill has no account, because Mintro does not guess where money comes from. Still not a scheduler: nothing reads these rows on a timer and a bill past its date sits there looking overdue until somebody calls this.

Example

curl -X POST 'https://api.mintro.money/v1/budgets/default/recurring/<id>/enter' \ -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.

post /v1/budgets/{budget_id}/recurring/enter-due

Enter every auto_enter bill that is due

scope budget:write · enterDueRecurring

Parameters

  • budget_id path · string · required

Body · JSON

  • due_by string · YYYY-MM-DD

Returns · 200

{ due_by, entered: [{ recurring_id, name, transaction_id, next_due, filed }], skipped: [{ recurring_id, name, reason, message }] }. The same batch the daily cron runs, on demand: every active bill marked auto_enter with next_due on or before due_by (default today) is entered through the same path as enter, one occurrence each, so a bill several periods behind catches up one call at a time. Idempotent: a date already in the register is skipped as already_entered. One bill failing (envelope closed, say) is reported and written to its auto_enter_error; the rest still enter. Bills without auto_enter are never touched; use enter for those.

Example

curl -X POST 'https://api.mintro.money/v1/budgets/default/recurring/enter-due' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "due_by": "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.

  • 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.

get /v1/budgets/{budget_id}/recurring/matches

Which bill an inbox row looks like

scope budget:read · suggestRecurringMatches

Parameters

  • budget_id path · string · required

  • transaction_id query · string

Returns · 200

{ matches: [{ transaction_id, due: [{ recurring_id, name, envelope_id, amount_cents, next_due, rank, days_off }], entered: [{ transaction_id, recurring_id, name, amount_cents, date, exact, days_off }], loans: [{ loan_id, name, payment_cents }], ambiguous }] }. A READ: nothing is filed, entered or advanced. loans names the open loans whose regular payment is within $2 of an outflow on their paying account; link one with POST /v1/transactions/{id}/loan-payment. For every unfiled row (or just transaction_id), the declared bills it could be (same account, same sign, due within three days or overdue), ranked: 1 exact cents, 2 the bill's name on the row and the amount within 20% of the bill, 3 the rest; and the bills already entered it could be the arrival of, only those at the exact amount, within $2, or sharing a payee word with the row, exact first, then close (the same payee within 20%); an unrelated bill is never offered. ambiguous: true means two candidates tie at the top and you must ask, never pick. A lone exact hit, or with no exact one a lone close one, was already adopted on arrival and does not appear here; what is left is ties, rows past 20%, and rows under another name.

Example

curl 'https://api.mintro.money/v1/budgets/default/recurring/matches' \ -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.

post /v1/budgets/{budget_id}/recurring/{id}/enter-from-bank

This bank row is the bill: file it and move the bill on

scope budget:write · enterRecurringFromBank

Parameters

  • budget_id path · string · required

  • id path · string · required

Body · JSON

  • transaction_id string · required · ≤ 64 chars

Returns · 200

{ recurring, transaction_id, filed }. No new transaction is written. The bank row is filed into the bill's envelope at the bank's amount (or, for income with no envelope, confirmed as ready to assign), linked to the bill (recurring_id, entered_via: bank_match), and next_due moves one step; the standing amount_cents is untouched. For a bill you already entered, use POST /v1/transactions/{id}/match instead; that replaces the placeholder. 400 validation_failed when the row is on another account, the wrong sign, a transfer, already filed, or already an occurrence.

Example

curl -X POST 'https://api.mintro.money/v1/budgets/default/recurring/<id>/enter-from-bank' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "transaction_id": "<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.

    envelope_closed: Closed envelopes reject new entries. File into an open envelope.

  • 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.

get /v1/budgets/{budget_id}/recurring

Bills you know are coming

scope budget:read · listRecurring

Parameters

  • budget_id path · string · required

  • due_by query · string

  • until query · string

Returns · 200

{ recurring: [{ id, name, envelope_id, account_id, amount_cents, cadence, next_due, moved_from, other_day, status, auto_enter, auto_enter_error, note, created_at }] }, soonest first. Cadence is the YNAB set: once, daily, weekly, every_other_week, twice_a_month, every_4_weeks, monthly, every_other_month, every_3_months, every_4_months, twice_a_year, yearly, every_other_year. other_day (1–31) is the other calendar day when cadence is twice_a_month, otherwise null. moved_from is the date the schedule had for next_due when just that date was moved (POST .../move), otherwise null. due_by=YYYY-MM-DD narrows to what falls on or before a date; pass the last day of the month to answer "what is still owed". until=YYYY-MM-DD adds occurrences: [dates] to every row: each date it falls due from next_due through until (at most 52), so a bi-weekly paycheque inside a month shows both paydays. A projection: nothing moves, and a paused row expands to none. Mintro runs only the ones marked auto_enter: once a day at 06:00 Mountain those are entered at their standing amount, one occurrence each, and auto_enter_error says why one was not. Everything else waits: next_due moves only when enter, advance or move is called. They exist so a budget can show what is coming before the money goes.

Example

curl 'https://api.mintro.money/v1/budgets/default/recurring' \ -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/budgets/{budget_id}/recurring

Declare a recurring bill

scope budget:write · createRecurring

Parameters

  • budget_id path · string · required

Body · JSON

  • name string · required · ≤ 200 chars

  • amount_cents integer · required · ≥ -9007199254740991

  • cadence "once" | "daily" | "weekly" | "every_other_week" | "twice_a_month" | "every_4_weeks" | "monthly" | "every_other_month" | "every_3_months" | "every_4_months" | "twice_a_year" | "yearly" | "every_other_year" · required

  • next_due string · required · YYYY-MM-DD

  • other_day integer | null · ≥ 1 · ≤ 31

  • envelope_id string | null · ≤ 64 chars

  • account_id string | null

  • loan_id string | null · ≤ 64 chars

  • holding_id string | null · ≤ 64 chars

  • auto_enter boolean

  • note string | null · ≤ 1000 chars

Returns · 201

The created row. holding_id says this bill buys into a holding: entering it adds the units the amount buys at the holding's last price and links the row; a monthly ETF purchase. loan_id says this bill pays a loan: entering it (by hand, by auto-enter, or when a bank row is adopted as it) files that row into envelope_id, links it to the loan and drops the principal in the same write, no second call; envelope_id is then required (a loan payment comes out of an envelope, as in YNAB) and the loan card says *paid by* this bill. auto_enter: true means it always hits this exact amount and Mintro may enter it on the day without asking; that needs an account, an envelope (or a holding) for a bill, and no envelope for income (which lands as ready to assign). 400 validation_failed says which is missing.

Example

curl -X POST 'https://api.mintro.money/v1/budgets/default/recurring' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "<name>", "amount_cents": 45000, "cadence": "once", "next_due": "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.

patch /v1/budgets/{budget_id}/recurring/{id}

Change or pause a recurring bill

scope budget:write · updateRecurring

Parameters

  • budget_id path · string · required

  • id path · string · required

Body · JSON

  • name string · ≤ 200 chars

  • amount_cents integer · ≥ -9007199254740991

  • cadence "once" | "daily" | "weekly" | "every_other_week" | "twice_a_month" | "every_4_weeks" | "monthly" | "every_other_month" | "every_3_months" | "every_4_months" | "twice_a_year" | "yearly" | "every_other_year"

  • next_due string · YYYY-MM-DD

  • other_day integer | null · ≥ 1 · ≤ 31

  • envelope_id string | null · ≤ 64 chars

  • account_id string | null

  • loan_id string | null · ≤ 64 chars

  • holding_id string | null · ≤ 64 chars

  • status "active" | "paused"

  • auto_enter boolean

  • note string | null · ≤ 1000 chars

Returns · 200

The updated row. status: paused keeps it without it counting as due. auto_enter toggles unattended entry and clears auto_enter_error; a paused bill is never auto-entered.

Example

curl -X PATCH 'https://api.mintro.money/v1/budgets/default/recurring/<id>' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "<name>", "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.

delete /v1/budgets/{budget_id}/recurring/{id}

Remove a recurring bill

scope budget:write · deleteRecurring

Parameters

  • budget_id path · string · required

  • id path · string · required

Returns · 204

204. Transactions already entered from it are untouched.

Example

curl -X DELETE 'https://api.mintro.money/v1/budgets/default/recurring/<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.

post /v1/budgets/{budget_id}/recurring/{id}/advance

Move a recurring bill on to its next occurrence

scope budget:write · advanceRecurring

Parameters

  • budget_id path · string · required

  • id path · string · required

Returns · 200

The updated row with the new next_due. Call this AFTER entering the transaction. It is the only thing that moves the date, and it moves it exactly one step. Month steps clamp to the end of a short month rather than rolling into the next one, so a bill due on the 31st is due on the 30th in November.

Example

curl -X POST 'https://api.mintro.money/v1/budgets/default/recurring/<id>/advance' \ -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/budgets/{budget_id}/recurring/{id}/move

Change when a recurring bill's next date enters

scope budget:write · moveRecurring

Parameters

  • budget_id path · string · required

  • id path · string · required

Body · JSON

  • date string · required · YYYY-MM-DD

  • scope "occurrence" | "series" · required

Returns · 200

The updated row. For a bill due on a Friday that will not come out until Monday. date (YYYY-MM-DD, today or later) is when the bill's next date now enters; next_due becomes it, so the forecast, cashflow, what is due and auto-enter all read the new date. scope: occurrence moves just this date and keeps the schedule: moved_from holds the date the schedule had, and the step after it lands on the usual day (monthly on the 9th, Oct 9 moved to Oct 12, next is Nov 9). It cannot reach the bill's following date (400 validation_failed says which date that is); move the series or skip instead. Moving it back onto moved_from clears the move. scope: series moves the schedule with it (Oct 12, then Nov 12), the same as PATCH next_due. A one-off bill has only one date, so either scope moves it. Entering, skipping, or editing the schedule forgets a moved date. Nothing is entered or filed. To move several bills, call this once per bill.

Example

curl -X POST 'https://api.mintro.money/v1/budgets/default/recurring/<id>/move' \ -H "Authorization: Bearer $MINTRO_KEY" \ -H 'Content-Type: application/json' \ -d '{ "date": "2026-09-15", "scope": "occurrence" }'

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.