Accounting
A feed that admits when something changed.
Bookkeeping software fails on corrections, not on new rows. This sync returns added, modified and removed as separate sets with a cursor, so create, amend and void each have an unambiguous trigger — and the enrichment that codes the line comes on the same key.
who this is for
Reconciliation is where bank feeds break.
Bookkeeping tools that need clean ledgers, not raw descriptors
what you'd call
The endpoints this actually uses.
Two loops: a sync that keeps the ledger true, and enrichment that codes what the sync brought in.
| POST | /v1/transactions/sync | added, modified and removed with a cursor |
| POST | /v1/transactions/get | Date-range backfill when onboarding a client |
| POST | /v1/accounts/get | Accounts, types, masks and balances to map to ledgers |
| POST | /v1/accounts/balance/get | Closing balance to reconcile a period against |
| POST | /v1/categories/get | The taxonomy to map to a chart of accounts once |
| POST | /v1/ai/categorize | Bulk coding, on a connection or a raw payload |
| POST | /v1/ai/merchant | Descriptor → merchant profile, for rules engines |
| POST | /v1/items/refresh | Force a pull before closing a period |
| POST | /v1/items/get | Connection health, so a stale feed is visible |
in code
What the integration looks like.
The reconciliation loop written the way a ledger needs it, then the coding pass that turns a descriptor into an entry.
import { FeelConnect } from "@feelconnect/node"; const fc = new FeelConnect({ apiKey: process.env.FEELCONNECT_SECRET_KEY!, baseUrl: "https://connect.feels.money/v1",}); // A ledger cannot tolerate "we think this changed". The sync returns// three explicit sets, so each one maps to a different ledger action.export async function reconcile(itemId: string, accessToken: string) { let cursor = await loadCursor(itemId); for (;;) { const page = await fc.transactions.sync({ access_token: accessToken, cursor, count: 500, }); // New bank lines → draft entries awaiting a match. for (const txn of page.added) await createDraftEntry(txn); // A pending charge settled at a different amount, or the bank // corrected a descriptor. Update in place; never double-post. for (const txn of page.modified) await amendEntry(txn.transaction_id, txn); // The bank withdrew the line. Void rather than delete, so the // audit trail survives the correction. for (const gone of page.removed) await voidEntry(gone.transaction_id); cursor = page.next_cursor; await saveCursor(itemId, cursor); // persist AFTER applying the page if (!page.has_more) break; }}// Coding a line: resolve the descriptor, then categorize in bulk.const { merchant } = await fc.ai.merchant({ query: "SQ *BLUE BOTTLE COFFEE OAK",}); // Bulk coding accepts a raw payload — no connection required, so this// also works on transactions imported from a client's CSV export.const { categorized, engine } = await fc.ai.categorize({ transactions: uncoded.map((line) => ({ id: line.id, description: line.description, amount: line.amount, })),}); // The taxonomy is static reference data, so your chart-of-accounts// mapping can be built once and stored rather than discovered per call.const { categories } = await fc.categories.get(); console.log(merchant, engine, categorized.length, categories.length);what you get
Mechanisms, in the order you would meet them.
Three sets, three ledger actions
addedcreates,modifiedamends,removedvoids. No diffing, no guessing, no double-posting when a pending charge settles.Cursors that survive a crash
Persistnext_cursorafter applying a page and an interrupted run replays that page rather than skipping it — the failure mode a ledger can actually tolerate.Merchant resolution as a first-class endpoint
A descriptor goes in, a merchant profile comes out — a far better key for your rules engine than the raw bank string.A taxonomy you map once
Primary categories with detailed subcategories, served as static reference data, so your chart-of-accounts mapping is stored rather than rediscovered on every call.Push instead of polling
transactions.sync_updates_available,item.errorandtransactions.initial_update— signed, timestamped, retried, and recorded per delivery.Client files as connections
CSV, OFX and QFX import runs for real with no provider agreement, so an unsupported institution becomes an upload rather than a lost client.
questions
Straight answers.
Other use cases
- AI agents and assistantsAn MCP server with 15 tools, an OpenAPI 3.1 document and an llms.txt — so an agent can discover the platform, connect an account and read real balances without you writing a tool wrapper.
- Budgeting and personal finance appsCursor-based transaction sync, categorization and subscription detection included — the exact stack feels.money runs on.
- Lending and underwritingIdentity, income streams, assets and liabilities from the source, with cash-flow forecasting and anomaly signals on top.
- Crypto and wealthBrokerage holdings, exchange balances and self-custody wallets in one holdings shape, with net worth computed across all of it.
- Platform pricingPublished tiers with the real quotas: Free for the sandbox, Startup at $99 for live access, Growth at $499, Enterprise quoted.
Ship a feed your users stop checking.
Run the reconciliation loop against the sandbox, force a modification, and watch it land as an amendment instead of a duplicate. That takes about ten minutes and no agreement with anyone.
The sandbox needs no approval and no card. You can have the shapes in front of you in a few minutes and decide from there.