Organize transactions
annotateCategorize, tag, rename, and organize transactions. Set party, category, transfer status, or ignored flag. Bulk operations: apply rules, detect recurring bills and subscriptions. All changes are reversible — raw bank data is never modified.
When reporting back to the user, translate counts and outcomes to plain English ("I categorized 14 transactions"). Don't echo schema field names.
ACTIONS:
Classify: categorize, set_party, tag, untag, annotate (set notes/url). Tags are normalized (lowercased, spaces→hyphens — "Greece Trip" stores as "greece-trip"); tag/untag input is normalized to match. untag is source-aware: removing a system (auto) tag also stops it from coming back on the next automatic refresh (re-tagging lifts the suppression); rule-applied tags are refused — edit or delete the rule instead.
Reserved tags: 'one-time' excludes a txn from recurring detection/forecast (effective next detection run, or immediately via detect_recurring); 'reimbursable'/'reimbursed' drive query { reimbursables: true }. 'refund' is the system's own marker for a returned charge.
Metadata: set_metadata (key+value; one value per key — set replaces), remove_metadata (key, optional value). Custom keys only: "tag"/"notes"/"receipt" are reserved and rejected with a pointer to their dedicated actions. Keys are trimmed + lowercased.
Flag: mark_transfer, mark_ignored, set_recurring
Revert: revert (clear manual overrides, restore import defaults)
Bulk: detect_recurring (creates/updates recurring_series — use preview:true to preview), apply_rules (applies saved rules — use preview:true to preview, rule_ids to limit which rules), infer_categories (fills a placeholder category from the party's own history elsewhere in the ledger; skips manual overrides and any party whose history isn't decisive; preview:true first). detect_recurring (always) and apply_rules with no filter/transaction_ids sweep the whole household: preview:true is a true dry run (no writes, no swept_at) and returns NO token — apply by re-running the same call without preview. NOTE filter.is_uncategorized will NOT find its targets — that means category IS NULL, and these rows carry a placeholder category. Select them with filter.category: ["Other Expense", "Other Income", "Uncategorized"] plus a date range COMPOUND: categorize accepts party_name/party_id to set both in one call. set_party accepts category_name/category_id/group_name to set both in one call.
TARGET: transaction_ids (max 5000) OR filter — one is required for most actions; if both are sent, the filter is ignored and a hint says so. detect_recurring takes no filter (use detect_start). FILTERS: filter.party, filter.category, filter.group, filter.tag, filter.exclude_tag are exact match (case-insensitive), accept string or string[]. filter.country ("GR" / "GRC" / "Greece") and filter.city select by where the merchant is. filter.account matches name or display name (exact, case-insensitive). filter.search is fuzzy text search across display name, party name, description, and check number. Date scope: filter.period (same named periods as query: this_month, last_month, ...) OR filter.start/end (YYYY-MM-DD) — not both. No date scope = ALL TIME. Also: amount_min/max, direction, is_uncategorized, is_recurring, is_transfer, is_ignored, is_pending, is_flagged, has_url, include_hidden. Unknown filter keys are rejected with a validation error (never silently dropped). Date keys at the TOP level (query's flat shape) are also rejected — they belong inside filter. TOKEN (filter-targeted writes only — explicit transaction_ids and filterless sweeps need none): call with preview:true → { matched, token, preview:[sample], will_set? }, review the matched rows and validated category/party/series targets (creates_party says whether a requested party name needs creation), then re-send the SAME action + filter WITH that token to execute (the token param says what makes one stale). Over 20000 matches is rejected: narrow filter.period/filter.account or pass explicit transaction_ids. Preview rows match detail: amt is signed (inflows positive, outflows negative), desc is the displayed name, raw is the original bank memo. RESPONSE: { affected: N, action, matched?, token?, will_set?: { category_id?, party_id?, party_name?, creates_party?, recurring_series_id? }, range?: { start, end } | "all", detail?: { ... }, warnings?: [...], hint? }. range echoes a filter write's effective date scope — check it matches your intent before and after bulk actions. REQUIRED PER ACTION: categorize needs category_id or category_name; set_party needs party_id or party_name; tag/untag need tag_name; annotate needs notes or url (notes:"" clears the note); set_recurring needs recurring_series_id; set_metadata needs key + value; remove_metadata needs key. mark_transfer/mark_ignored/revert need no payload. A missing one is rejected up front, before any write or preview. When affected=0, an error field explains why (e.g. party not found, category not found, missing required field, no matching transactions, missing/stale token) — or, when the requested state already holds (re-applying a tag), a hint says so. When overriding a rule-classified transaction (≤5 explicit IDs, categorize/set_party/mark_transfer/mark_ignored), warnings include rule details and recovery instructions. The write always succeeds; warnings are advisory. NOTE: To explain/debug a transaction's classification, use query { explain: true, transaction_id: "" } instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| end | No | Do not use: end belongs inside filter: { filter: { end: ... } } | |
| key | No | Metadata key (for set_metadata/remove_metadata) | |
| url | No | Receipt URL to attach | |
| notes | No | Notes to add | |
| start | No | Do not use: start belongs inside filter: { filter: { start: ... } } | |
| token | No | REQUIRED to execute a filter-based write. Get it from a prior preview:true call (the response includes `token`) and pass it back unchanged. It binds the action + the exact set of matched transactions, so the write is rejected if that set, the scope, or the household changed since the preview (including a same-count swap). Not needed when targeting explicit transaction_ids, nor for detect_recurring or a filterless apply_rules (their previews return no token). | |
| value | No | Metadata value (for set_metadata/remove_metadata) | |
| action | Yes | Action to perform | |
| filter | No | Target transactions by filter | |
| period | No | Do not use: period belongs inside filter: { filter: { period: ... } } | |
| dry_run | No | Do not use: use preview: true (there is no dry_run param) | |
| preview | No | Preview without writing: matched transactions and validated will_set targets for filter-based targeting (creates_party reports whether a requested party name needs creation), or for detect_recurring the series it would create/update/deactivate and the links it would add/clear. Only a filter-targeted preview returns a token | |
| group_id | No | Category group ID | |
| party_id | No | Party to assign | |
| rule_ids | No | Limit which rules to apply | |
| tag_name | No | Tag name | |
| group_name | No | Category group name | |
| is_ignored | No | Mark as ignored (excluded from all reports) | |
| party_name | No | Party name (creates if needed) | |
| party_type | No | Party type classification | |
| category_id | No | Category to assign | |
| is_transfer | No | Mark as transfer | |
| detect_start | No | Start date for detect_recurring lookback (default: 18 months ago). A narrower window re-estimates every matched series from less history and UNLINKS its auto-linked charges older than the window — preview:true first | |
| category_name | No | Category name — must be an existing category (the taxonomy is a fixed set; use admin category list) | |
| transaction_ids | No | Target specific transactions by ID | |
| overwrite_manual | No | Overwrite manual enrichments when applying rules | |
| recurring_series_id | No | Link to recurring series |