Skip to content
WealthMgr Docs

Endpoint Reference

All API key requests must include an Authorization: Bearer <token> header. See API Overview for how to generate a key.

API key access requires a Starter or higher plan. Free tier requests return 403 Forbidden. Each plan includes a monthly call quota.

Accounts

GET /api/accounts

Returns all accounts for the authenticated user. Sign convention (WEA-249 / D11): `balance` is the signed stored value — liabilities carry the NEGATIVE amount owed (`-500.00` = owing 500), so net worth is the plain sum of all balances. `openingStatementDate` (WEA-473) is the day-key of the account's latest opening-statement anchor — the boundary below which transaction legs stop folding into `balance`; `null` means the account has no anchor and every leg folds. See Balance folding below.

Parameters

NameInTypeRequiredDescription
includeArchived query boolean No Include archived accounts (default: false)

Response

[
  {
    "id": "acc_abc123",
    "name": "Current Account",
    "type": "asset",
    "currency": "GBP",
    "balance": 2450.00,
    "subtypeId": "sub_checking",
    "subtype": {
      "id": "sub_checking",
      "name": "checking",
      "builtInId": "checking"
    },
    "starred": false,
    "archivedAt": null,
    "closingDay": null,
    "openingStatementDate": "2024-01-01",
    "createdAt": "2024-01-15T10:30:00.000Z",
    "updatedAt": "2024-03-20T14:22:00.000Z"
  }
]
GET /api/accounts/:id

Returns a single account by ID. Same shape as the list route, including `openingStatementDate` (null for legacy no-anchor accounts).

Response

{
  "id": "acc_abc123",
  "name": "Current Account",
  "type": "asset",
  "currency": "GBP",
  "balance": 2450.00,
  "subtypeId": "sub_checking",
  "subtype": {
    "id": "sub_checking",
    "name": "checking",
    "builtInId": "checking"
  },
  "starred": false,
  "archivedAt": null,
  "closingDay": null,
  "openingStatementDate": "2024-01-01",
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-03-20T14:22:00.000Z"
}
POST /api/accounts

Creates a new account. Only `asset` and `liability` types can be created via the API. Sign convention (WEA-249 / D11): `balance` uses the signed stored convention, symmetric with GET — a liability's balance must be zero or negative, where `-500` means owing 500; positive liability values are rejected with 400. The account is born with its opening anchor transaction in one atomic write, so its balance always equals its ledger. The `201` body carries `openingStatementDate`: the creation day when a non-zero balance wrote the anchor, `null` when a zero-balance account is born without one.

Request body

{
  "name": "N26 Current",
  "type": "asset",
  "subtype": "bank_account_savings",
  "currency": "EUR",
  "balance": 1200.50
}

Response

{
  "id": "acc-api-1759040123456-abcd1234-usr12345",
  "name": "N26 Current",
  "type": "asset",
  "currency": "EUR",
  "balance": 1200.50,
  "subtypeId": "d1f2...",
  "subtype": {
    "id": "d1f2...",
    "name": "Savings Account",
    "builtInId": "bank_account_savings"
  },
  "starred": false,
  "archivedAt": null,
  "closingDay": null,
  "openingStatementDate": "2026-09-29",
  "createdAt": "2026-09-29 02:15:12",
  "updatedAt": "2026-09-29 02:15:12"
}

Creating accounts — fields

FieldRequiredDefaultDescription
nameYes—Account name (non-empty). Names are not unique — see the retry caveat below.
typeYes—asset or liability only (API v1).
currencyYes—Must be one of the currencies available in your account (the Configuration page’s list, plus your persisted primary currency). Case-insensitive; stored uppercase.
subtypeOne of subtype / subtypeId—Built-in subtype id, e.g. bank_account_savings, credit_card, bank_loan_mortgage.
subtypeId——Or the exact account_subtype row id from your account (visible on GET /api/accounts).
balanceNo0Signed stored opening. Assets: any number. Liabilities: zero or negative — {"type":"liability","balance":-500} means owing 500 (WEA-249 / D11); a positive liability value is a convention violation and returns 400.
closingDayNonullCredit-card statement closing day, 1–31.

A non-zero balance writes an Actual balance anchor transaction (internally trx_origin: 'initial_balance') so the ledger sums to the stated opening from birth; a zero balance creates no anchor (an empty ledger).

Fields deliberately not settable via the API: sortOrder, autopayment fields, pockets, and user id.

Since WEA-368 the API also exposes PATCH /api/accounts/:id and DELETE /api/accounts/:id (see below) for corrections — starred, archivedAt, name, subtype, currency, and closing day are editable there. Balance restatement stays app-only by design: a balance is backed by the opening-anchor flow (money-truth rule D11), so PATCH with a balance field returns 400; correct the transactions instead and the balance recomputes.

Warning

**Retry duplicates:** `POST /api/accounts` has no name-based dedupe (reusing a name across institutions is legitimate). A retried request — e.g. after a timeout or 5xx — creates a **second account** with the same name. Keep the account `id` from each successful `201` response and re-check `GET /api/accounts` before retrying an ambiguous write.

Worked liability example — a credit card with 500 outstanding:

{
  "name": "Visa Cash Plus",
  "type": "liability",
  "subtype": "credit_card",
  "currency": "USD",
  "balance": -500,
  "closingDay": 28
}

{"type":"liability","balance":-500} means owing 500 — the same value GET /api/accounts returns for it. Positive values are rejected.

PATCH /api/accounts/:id

Updates an existing account (WEA-368). Only the fields you send change. Editable: name, subtype/subtypeId (same resolution as POST), currency (must be available in your account), closingDay (1–31 or null), starred, archivedAt (ISO string to archive, null to un-archive). type may be flipped asset↔liability ONLY while the account has no ledger rows at all (opening anchors count) — otherwise 409. balance is NOT editable: restating it runs through the app's opening-anchor flow (D11); send transaction corrections instead.

Request body

{
  "name": "N26 Current (fixed)",
  "currency": "EUR",
  "starred": true,
  "subtype": "bank_account_checking"
}

Response

// 200 — the full updated account object (same shape as GET /api/accounts/:id, updatedAt reflects the change)
// 400 — validation failure, "balance" present, or no editable fields provided
// 404 — account not found
// 409 — type flip refused because ledger rows exist
DELETE /api/accounts/:id

Deletes an account and its own opening-balance anchor (WEA-368). Refused with 409 while the account still has: real transactions on either leg, budget pockets, automation-rule/alert conditions, or rules using it as from/to account. The error message names the blocker so an importer can clear it first.

Response

// 204 No Content on success
// 404 if unknown

Transactions

GET /api/transactions

Returns transactions for the authenticated user. Supports pagination and filtering. Each row carries `foldsIntoBalance` (WEA-473): whether this row's leg moves the account's `balance` — a boolean when the request filters by `accountId`, `null` on unfiltered requests (the fold is decided per account-leg, so there is no single answer without one). See Balance folding below.

Parameters

NameInTypeRequiredDescription
accountId query string No Filter by from or to account ID
from query string No Start date (ISO 8601, e.g. 2024-01-01)
to query string No End date (ISO 8601, e.g. 2024-03-31)
limit query number No Max results (default 50, max 200)
offset query number No Pagination offset (default 0)

Response

{
  "transactions": [
    {
      "id": "txn_abc",
      "fromAccountId": "acc_abc123",
      "toAccountId": null,
      "fromAmount": 45.00,
      "fromCurrency": "GBP",
      "toAmount": 45.00,
      "toCurrency": "GBP",
      "fxRate": null,
      "description": "Groceries",
      "date": "2024-03-15",
      "time": null,
      "trxOrigin": "api",
      "createdAt": "2024-03-15T12:30:00.000Z",
      "fxDegradedAt": null,
      "fxStaleRateDate": null,
      "foldsIntoBalance": true
    }
  ],
  "total": 142
}

Balance folding (reconciling a listing against balance)

An account’s stored balance is not the plain sum of its transaction rows — it is maintained by the statement-fold law (WEA-320/340), and GET /api/transactions exposes exactly which rows participate (WEA-473):

balance = Σ(statement legs) + Σ(flows with foldsIntoBalance: true)
  • The anchor: openingStatementDate on the account (from GET /api/accounts) is the day-key S of its latest Actual balance (opening-statement) row — the number the user stated. Accounts created with a non-zero balance always have one.
  • Flows after S fold (foldsIntoBalance: true); flows dated before S are still listed and stored but never fold — they live under the stated number (false, the “accept-below” rule that prevents double-counting pre-statement activity).
  • Same-day flows fold only when they were recorded strictly after the anchor row (created_at ordering) — you stated the balance, then booked more activity on top of it. Identical or unusable timestamps decide false (the safe direction).
  • Statement legs always fold regardless of date: opening anchors, bank-reconciliation plugs, and price_revaluation restatements define or restate the position rather than flow underneath it.
  • No anchor (openingStatementDate: null) — legacy or zero-balance accounts with no stated position: every leg folds, so the listing sum equals the balance (with signs: a from-leg subtracts its fromAmount, a to-leg adds its toAmount).
  • foldsIntoBalance: null simply means the request had no accountId filter; the fold is a property of a row’s leg on one account. Pass ?accountId= to get per-row booleans.

So an importer can verify a sync: fetch the account (balance, openingStatementDate), page through GET /api/transactions?accountId=…, and sum only the foldsIntoBalance: true legs — the result must equal balance. Rows listed as false are not invisible or corrupt; they are history recorded under the stated balance.

POST /api/transactions

Creates a new transaction. Use fromAccountId for expenses (money leaving an account) and toAccountId for income (money entering). Set both for transfers between accounts. Supports installment purchases — pass isInstallmentPayment: true to automatically create a rule that handles all remaining monthly payments.

Request body

{
  "fromAccountId": "acc_abc123",
  "toAccountId": null,
  "fromAmount": 45.00,
  "fromCurrency": "GBP",
  "date": "2024-03-15",
  "description": "Groceries"
}

Response

{
  "id": "txn_xyz",
  "installmentRuleId": null
}

Duplicate detection

Every POST /api/transactions request is checked against existing transactions before inserting. There are two levels:

Hard duplicate → 409 Conflict

If all of the following fields match an existing transaction exactly, the request is rejected and no row is inserted:

fromAccountId, toAccountId, date, time, fromAmount, fromCurrency, toAmount, toCurrency, description

null values are matched exactly — a transaction with no time only conflicts with another transaction that also has no time.

{
  "error": "Duplicate transaction",
  "existingId": "txn_abc"
}

Soft duplicate → 202 Accepted

If only date and fromAmount match an existing transaction (but the full field set does not), the transaction is inserted and flagged for review. It will appear in the Review page under Possible Duplicates, where you can choose to keep or delete it.

{
  "id": "txn_xyz",
  "installmentRuleId": null,
  "possibleDuplicateOf": "txn_abc",
  "duplicateLinkId": "dl_123"
}

A 202 response means the transaction was saved — it is not an error. Your integration should treat 201 and 202 as both successful.

For installment purchases (e.g. a credit card purchase paid in monthly instalments), include the installment fields:

FieldRequiredDefaultDescription
isInstallmentPaymentNofalseMark this as an installment purchase
numberOfInstallmentsWhen isInstallmentPayment: true—Total number of monthly payments
currentInstallmentNo1Which payment this transaction represents. 1 = first payment. Pass 3 to record the 3rd payment of an existing purchase — the system creates a rule starting from payment 4.
yearlyInterestRateNo0Annual interest rate as a percentage. 0 = equal split. Uses standard amortization.

fromAmount must always be the full original purchase price (not the per-installment amount).

When currentInstallment < numberOfInstallments, the response includes an installmentRuleId pointing to the newly created rule that will handle all remaining payments.

{
  "fromAccountId": "acc_credit_card",
  "toAccountId": "acc_expense",
  "fromAmount": 1200.00,
  "fromCurrency": "USD",
  "date": "2026-04-19",
  "description": "Laptop",
  "isInstallmentPayment": true,
  "numberOfInstallments": 12,
  "currentInstallment": 1,
  "yearlyInterestRate": 18.5
}

Response when an installment rule is created:

{
  "id": "txn_xyz",
  "installmentRuleId": "rule-abc123"
}
PATCH /api/transactions/:id

Corrects an existing transaction (WEA-368). Editable: fromAccountId/toAccountId (null or empty string clears a leg — at least one leg must remain), fromAmount/toAmount, fromCurrency/toCurrency, fxRate (number or null), description (string or null), date (YYYY-MM-DD), time (HH:MM or null), tags (array — REPLACES the current tag set; [] clears). Corrected money legs run through the same booking-time currency guardrail as POST: a to-leg whose currency differs from the destination account's currency converts at that day's rate (fxStaleRateDate records a prior-day fallback; an unresolvable pair posts at face with fxDegradedAt — never a silent 1:1). The server unwinds the OLD balance folds and re-applies the new ones, and re-anchors the next bank-reconciliation plug on each affected account. Patching a row previously booked degraded re-runs the conversion and self-heals the markers. Not editable: initial_balance anchors and price_revaluation rows (409); pocket-allocated rows accept metadata edits (date/time/description/tags) but reject money-leg edits (409).

Request body

{
  "fromAccountId": "acc_abc123",
  "fromAmount": 54.00,
  "date": "2024-03-16",
  "description": "Corrected: Groceries",
  "tags": ["food"]
}

Response

// 200 — the corrected transaction (same shape as GET /api/transactions items,
//   including fxDegradedAt / fxStaleRateDate)
// 400 — validation failure, no editable fields, or clearing legs would leave none
// 404 — transaction not found
// 409 — guarded row (anchor / price-revaluation / pocket money edit)
DELETE /api/transactions/:id

Deletes a transaction and reverses its effect on account balances (WEA-368 hardening: only folds that were actually applied are unwound, the next reconciliation plug is re-anchored, and tag/duplicate/rule references to the row are cleaned up). Guarded rows return 409: initial_balance anchors (restate in the app or delete the account), price_revaluation rows (system-generated), and pocket-allocated transactions (delete in the app so allocation deltas unwind consistently).

Response

// Returns 204 No Content on success

Returns 404 Not Found if the transaction does not exist or belongs to another user.

Exchange rates

GET /api/rates

Returns the exchange rate between two currencies. Rates are updated daily.

Parameters

NameInTypeRequiredDescription
from query string Yes Source currency code (e.g. USD)
to query string Yes Target currency code (e.g. GBP)

Response

{ "rate": 0.79143 }