Dough API

57 endpoints over HTTP, on your own instance, authenticated with a key you mint yourself. Your finances are yours: read them, write them, move them elsewhere.

Getting started

Every endpoint lives under /api/v1 on the instance you run. Responses are JSON, amounts are in euros, and a month parameter is always YYYY-MM, defaulting to the current month in the server’s timezone.

curl -s https://dough.example.com/api/v1/summary \
  -H "Authorization: Bearer $DOUGH_API_KEY"

Authentication

Create a key in the app under Settings, then API keys. It is shown once, because only its SHA-256 hash is stored: the database never holds a usable secret. Send it as a bearer token, or as x-api-key.

Authorization: Bearer dough_xxxxxxxx

A key carries read, or read and write. A read key can see everything and change nothing. A missing or revoked key returns 401; a key without the scope an endpoint needs returns 403. Revoking a key in settings stops it working on its next request.

On an instance without the settings page, a key can still be minted on the machine that owns the database:

npx tsx scripts/create-api-key.ts --name "my-client" --scopes read,write

Sign in with OAuth

An app can ask a person to sign in to their own instance instead of asking them for a key. Every Dough is its own authorization server, so there is nothing to register with anybody: the app reads /.well-known/oauth-authorization-server from whatever host it was pointed at, registers itself at /api/oauth/register, and sends the person to /oauth/authorize to approve it.

The flow is authorization code with PKCE, which is required: S256 only, no client secrets, no implicit grant. Codes are single use and expire in two minutes; reusing one revokes every token it produced. Access tokens last an hour, refresh tokens rotate on use, and a rotated refresh token cannot be used again.

GET /.well-known/oauth-authorization-server
POST /api/oauth/register     {"client_name": "...", "redirect_uris": ["yourapp://connect"]}
GET /oauth/authorize?client_id=...&redirect_uri=...&response_type=code
    &scope=read+write&state=...&code_challenge=...&code_challenge_method=S256
POST /api/oauth/token        grant_type=authorization_code&code=...&code_verifier=...
POST /api/oauth/token        grant_type=refresh_token&refresh_token=...
POST /api/oauth/revoke       token=...

The token that comes back authenticates every endpoint below exactly as an API key does, and carries the same read and write scopes. Revoke it from the app or from Settings.

Overview

The two calls that answer how things stand, without walking every account.

GET/api/v1/summaryread

Total balance, and the month's income, budgeted, activity and Ready to Assign. The first call an assistant reaches for.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.

GET/api/v1/net-worthread

Net worth by kind (checking, savings, investments, debts) plus the saved snapshot history.

GET/api/v1/ai-statusread

Each AI service (Dougie, receipts, categorizing, summaries) with its provider and model, its state (ok, failing with the last error, or off) and the share of this month's AI allowance used, null when unlimited.

Dashboard

Everything the dashboard shows, in one call: the daily budget and the reason behind it, what is left today, the obligations ahead, the charts and the month's figures. Computed by the same module the web dashboard reads, so two clients looking at one instance cannot disagree.

GET/api/v1/dashboardread

The whole dashboard model: daily budget with its breakdown and notice, today's spending and what is left, upcoming bills and income, spending flow against pace, categories, streak, trends, cash flow, recent transactions and net worth.

Accounts

Balances as the app sees them. budget_excluded marks accounts left out of the spendable-balance figure, so a client can show the same number the UI does.

GET/api/v1/accountsread

Every open account with its balance.

Query parameters

  • include_closed1Also return closed accounts.

POST/api/v1/accounts/createwrite

Add a manual account.

Body

  • namestringrequiredWhat the account is called.
  • typestringchecking, savings, otherAsset or otherDebt. Defaults to checking.
  • balancenumberSeeds both balance and cleared balance.
  • on_budgetbooleanDefaults to true.

POST/api/v1/accounts/updatewrite

Edit an account. Only the fields sent change. Setting balance records a reconciliation transaction, so history keeps adding up.

Body

  • idstringrequiredAccount id.
  • namestring
  • typestring
  • balancenumber
  • on_budgetboolean
  • closedboolean
  • sort_ordernumber

POST/api/v1/accounts/deletewrite

Manual accounts are removed. Synced accounts are closed instead, because their history is not ours to drop.

Body

  • idstringrequiredAccount id.

POST/api/v1/accounts/reorderwrite

Set the order accounts are listed in.

Body

  • orderstring[]requiredAccount ids, in the order wanted.

Transactions

The ledger. Writes here are local mode only: in YNAB mode transactions belong to YNAB and must be edited there.

GET/api/v1/payeesread

The payees this household uses, most used first, each with the category it is usually filed under, the account it is usually paid from, what it last cost and its last description. A client can complete an entry from this without asking again, and it keeps working offline. The category is stated only when the history agrees, so an inconsistently filed payee gets none rather than a wrong guess.

Query parameters

  • limitnumber1 to 1000, default 300.

GET/api/v1/payees/merge-suggestionsread

Payees that look like one merchant written several ways, as groups of { into, from }, proposed by the quick AI model from the 300 most used payees. Asking merges nothing.

POST/api/v1/payees/mergewrite

Rename every transaction under the names in from to into. A rename is a merge of one name. Local mode only.

Body

  • fromstring[]requiredThe payee names to replace.
  • intostringrequiredThe name they all take.

GET/api/v1/transactionsread

Transactions newest first.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.
  • account_idstringOne account.
  • categorystringExact category name.
  • qstringSearch payee, category, memo, account name, or the amount when a number (12,50 or 12.50).
  • limitnumber1 to 500, default 50.

POST/api/v1/transactions/createwritelocal mode only

Add a transaction and apply its balance effect. Meant for rows the bank import has not seen yet, most of all pending card holds.

Body

  • account_idstringrequiredWhich account it hits.
  • amountnumberrequiredThe absolute value; inflow decides the direction.
  • inflowbooleanDefaults to false, money out.
  • dateYYYY-MM-DDDefaults to today.
  • payee_namestring
  • memostring
  • categorystringCategory name.
  • clearedstring
  • transfer_account_idstringWith category "Internal transfer", creates the counterpart leg.

POST/api/v1/transactions/updatewritelocal mode only

Patch one transaction. Only the fields sent change.

Body

  • transaction_idstringrequiredThe id the read endpoints return.
  • amountnumberAbsolute value; inflow true stores it positive.
  • inflowboolean
  • payee_namestring
  • memostring
  • account_idstring
  • dateYYYY-MM-DD
  • categorystring
  • transfer_account_idstringFixes a misrouted transfer and maintains the opposite leg.

POST/api/v1/transactions/deletewritelocal mode only

Remove a transaction, split siblings included, and reverse its balance effect.

Body

  • transaction_idstringrequired

Budget

Envelope budgeting: what each category has been given, spent and has left. The same maths as the budget page, carry-forward included.

GET/api/v1/badgesread

The navigation's notice dots: budget_overspent (categories overspent this month), transactions_unread (1 when an expense was added by hand since this key's user last looked) and chat_unread (messages from others since then).

POST/api/v1/badgeswrite

Clear a dot when its screen is opened. The budget dot clears only when nothing is overspent.

Body

  • seenstringrequiredtransactions or chat.

GET/api/v1/budgetread

The month's income, total budgeted, Ready to Assign, age of money, every active category's budgeted, activity and available, and the hidden categories.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.

POST/api/v1/budget/assignwrite

Set, not add, one category's budgeted amount for a month.

Body

  • monthYYYY-MMDefaults to the current month in the server's timezone.
  • category_idnumberEither this or category_name.
  • category_namestring
  • budgetednumberrequiredThe amount the category ends up with.

POST/api/v1/budget/unassignwrite

Clear over-assigning: take back unspent money from this month's assignments until Ready to Assign is zero, first what sits beyond a target, then categories without a target, then targeted ones. Nothing already spent is undone.

Body

  • monthstringYYYY-MM, default this month.

GET/api/v1/budget/auto-assignread

Preview funding targets from Ready to Assign. Without mode, the total each mode would assign; with mode, the full per-category plan. Writes nothing.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.
  • modestringunderfunded, last_assigned, last_spent, or smart (overspending first, then targets, most-used categories first).

POST/api/v1/budget/auto-assignwrite

Apply a plan. Capped so it never overbudgets.

Body

  • monthYYYY-MMDefaults to the current month in the server's timezone.
  • modestringrequiredunderfunded, last_assigned, last_spent, or smart (overspending first, then targets, most-used categories first).

POST/api/v1/budget/movewrite

Move assigned money from one category to another within a month.

Body

  • monthYYYY-MMDefaults to the current month in the server's timezone.
  • from_category_idnumberrequired
  • to_category_idnumberrequired
  • amountnumberrequired

POST/api/v1/budget/snoozewrite

Snooze a category for a month, so its target stops asking to be funded.

Body

  • category_idnumberrequired
  • monthYYYY-MMDefaults to the current month in the server's timezone.

POST/api/v1/budget/unsnoozewrite

Remove a category's snooze for a month.

Body

  • category_idnumberrequired
  • monthYYYY-MMDefaults to the current month in the server's timezone.

Categories

The envelopes themselves, and the targets that decide what auto-assign funds.

GET/api/v1/categories/suggestread

The categories a picker should offer first for an entry: guess is the single best pick (a payee and amount always filed the same way, else the AI's), ranked the categories this payee or description is most often filed under.

Query parameters

  • payeestringThe payee as typed.
  • memostringThe description, which often tells a generic payee apart.
  • amountnumberThe amount, for the payee and amount history.

POST/api/v1/categories/createwrite

Add a category.

Body

  • namestringrequired
  • group_namestringThe group it sits under.
  • colorstring

POST/api/v1/categories/updatewrite

Edit a category, or link it to a subscription, bill, debt account, savings goal or investment account.

Body

  • idnumberrequiredPlus the fields to change.

POST/api/v1/categories/deletewrite

Delete a category. One with transactions needs reassign_to, and its monthly budgets merge into that target.

Body

  • idnumberrequired
  • reassign_tonumberAnother category id.

POST/api/v1/categories/targetwrite

Set or clear a category's budget target.

Body

  • category_idnumberrequired
  • monthly_amountnumberRequired unless clearing.
  • cadencestringdaily, weekly, monthly, yearly or by_date.
  • target_dateYYYY-MM-DDWith cadence by_date.
  • due_daynumberWith cadence monthly: the day of the month it is due by, 1-31. null clears it.
  • snooze_until_monthYYYY-MM
  • clearbooleantrue removes the target.

Bills

Recurring obligations with a due day, monthly or yearly.

GET/api/v1/billsread

Every bill with amount, due day and cadence.

POST/api/v1/bills/createwrite

Add a recurring bill.

Body

  • namestringrequired
  • amountnumberrequired
  • due_daynumberrequired1 to 31.
  • categorystring
  • cadencestringmonthly or yearly.
  • interval_monthsnumberEvery how many months: 1 monthly, 12 yearly.
  • due_monthnumber1 to 12, the month it next falls in when the interval is over one.

POST/api/v1/bills/updatewrite

Edit a bill, or mark this month's instance paid.

Body

  • idnumberrequired
  • mark_paidbooleanMarks the current month paid.
  • paid_amountnumberWhat was actually paid.
  • is_priorityboolean
  • is_activeboolean

POST/api/v1/bills/deletewrite

Delete a bill with its status, history and patterns.

Body

  • idnumberrequired

Subscriptions

The smaller recurring charges, kept apart from bills so they can be counted on their own.

GET/api/v1/subscriptionsread

Every subscription with amount, due day, how many months apart it falls (interval_months, due_month), whether it falls this month (due_this_month) and whether this month's charge is paid.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.

POST/api/v1/subscriptions/createwrite

Add a subscription.

Body

  • namestringrequired
  • amountnumberrequired
  • due_daynumberrequired1 to 31.
  • interval_monthsnumberEvery how many months: 1 monthly (default), 12 yearly.
  • due_monthnumber1 to 12, the month it next falls in when the interval is over one.
  • brand_colorstring
  • brand_logostring

POST/api/v1/subscriptions/updatewrite

Edit a subscription, or mark this month's charge paid. Only the fields sent change.

Body

  • idnumberrequired
  • namestring
  • amountnumber
  • due_daynumber1 to 31.
  • interval_monthsnumberEvery how many months: 1 monthly, 12 yearly.
  • due_monthnumber1 to 12, the month it next falls in when the interval is over one.
  • brand_colorstring
  • brand_logostring
  • is_prioritybooleanA must-pay subscription.
  • is_activeboolean
  • mark_paidbooleanThis month's charge paid or not.

POST/api/v1/subscriptions/deletewrite

Delete a subscription.

Body

  • idnumberrequired

Income

What is expected to arrive, and whether it has. upcoming is true for an active source not yet received whose day is still ahead.

GET/api/v1/incomeread

Income sources with the month's received and upcoming status.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.

POST/api/v1/income/createwrite

Add an income source.

Body

  • namestringrequired
  • amountnumberrequired
  • expected_daynumberrequiredDay of month it usually lands.
  • is_recurringboolean
  • target_account_idstringWhere it arrives.

POST/api/v1/income/updatewrite

Edit an income source, or mark it received this month.

Body

  • idnumberrequired
  • mark_receivedboolean

POST/api/v1/income/deletewrite

Delete an income source.

Body

  • idnumberrequired

Savings goals

Saved is the linked category's available balance, assigned minus spent and carried forward, never the lifetime sum of assignments.

GET/api/v1/savings-goalsread

Active goals with target and derived saved amount.

Query parameters

  • monthYYYY-MMDefaults to the current month in the server's timezone.

POST/api/v1/savings-goals/createwrite

Add a goal, optionally linked to a budget category.

Body

  • namestringrequired
  • target_amountnumberrequired
  • ynab_category_idstringLink to a category by id.
  • ynab_category_namestringOr by name.
  • target_dateYYYY-MM-DD
  • descriptionstring

POST/api/v1/savings-goals/updatewrite

Edit a goal. Only the fields sent change.

Body

  • idnumberrequired
  • include_in_calculationsboolean
  • is_activeboolean

POST/api/v1/savings-goals/deletewrite

Delete a goal and clear its budget link.

Body

  • idnumberrequired

Debts

Open otherDebt accounts with the interest rate, minimum payment and due day kept beside them.

GET/api/v1/debtsread

Every open debt with its override fields, the payoff plans for an extra monthly payment (extra=), and months_to_debt_free_at_minimums: when the minimums alone clear everything, or null when they never do.

POST/api/v1/debts/updatewrite

Set a debt's override fields.

Body

  • ynab_account_idstringrequiredWhich debt.
  • interest_ratenumberAnnual percentage.
  • minimum_paymentnumber
  • due_daynumber
  • original_amountnumber
  • notesstring
  • is_priorityboolean

POST/api/v1/debts/reorderwrite

Set the display order.

Body

  • orderstring[]requiredAccount ids, in the order wanted.

Investments

otherAsset accounts with what goes in monthly and what return is assumed.

GET/api/v1/investmentsread

Every investment account with its overrides.

POST/api/v1/investments/updatewrite

Edit an override, or re-value the account.

Body

  • ynab_account_idstringrequiredWhich account.
  • monthly_contributionnumber
  • expected_returnnumberAnnual percentage.
  • valuenumberRe-value the account to this.
  • addednumberMoney put in alongside the re-valuation.
  • tickerstring
  • notesstring

POST/api/v1/investments/reorderwrite

Set the display order.

Body

  • orderstring[]requiredAccount ids, in the order wanted.

Account

The household's data to take away, and deleting the account the key belongs to.

GET/api/v1/account/exportwrite

The household's data as one SQLite file, the format Dough runs on, so it opens anywhere and can start a self-hosted instance. Left out: API keys, OAuth tokens, the saved bank and AI keys and the members' passwords. The export in the app's Settings keeps the saved keys and passwords.

POST/api/v1/account/deletewrite

Delete the key owner's account. Their chats, keys and tokens go; what they added to the household passes to the next member. The last member's deletion removes the whole household. Answers { deleted: "left-household" | "erased-household" }, or 403 for a wrong password.

Body

  • passwordstringrequiredThe account's password, as confirmation.

Errors

Errors are JSON with an error string. 400 means the body was wrong, or the write is not available in the instance’s current mode. 401 means the key is missing, wrong or revoked. 403 means the key lacks the scope. 404 means the row is not there. A client should degrade per endpoint rather than per app: a read key is perfectly useful, and so is an instance in YNAB mode where transaction writes are refused.

MCP server

The Dough MCP server is a thin client of this API: point it at your instance with an API key and an assistant can answer questions about your finances. It exposes each endpoint here as a tool.