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

Every accounting product hits the same wall. The feed delivers a pending card authorisation; three days later it settles two dollars higher. If the API only ever hands you new rows, you have a duplicate, a manual fix, and a user who no longer trusts the feed.

The other wall is coding. Raw bank descriptors are hostile — prefixes, store numbers, processor noise — and a bookkeeper who has to rename every line is not saving any time. What you want is a clean merchant name and a category suggestion good enough to accept by default.

Both are in the same integration here, on the same key, with the enrichment metered separately so it does not eat your sync budget.

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.

Endpoints used by accounting and bookkeeping integrations
POST/v1/transactions/syncadded, modified and removed with a cursor
POST/v1/transactions/getDate-range backfill when onboarding a client
POST/v1/accounts/getAccounts, types, masks and balances to map to ledgers
POST/v1/accounts/balance/getClosing balance to reconcile a period against
POST/v1/categories/getThe taxonomy to map to a chart of accounts once
POST/v1/ai/categorizeBulk coding, on a connection or a raw payload
POST/v1/ai/merchantDescriptor → merchant profile, for rules engines
POST/v1/items/refreshForce a pull before closing a period
POST/v1/items/getConnection 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.

tsreconcile.ts
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;  }}
tscoding.ts
// 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.

  1. Three sets, three ledger actions

    added creates, modified amends, removed voids. No diffing, no guessing, no double-posting when a pending charge settles.
  2. Cursors that survive a crash

    Persist next_cursor after applying a page and an interrupted run replays that page rather than skipping it — the failure mode a ledger can actually tolerate.
  3. 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.
  4. 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.
  5. Push instead of polling

    transactions.sync_updates_available, item.error and transactions.initial_update — signed, timestamped, retried, and recorded per delivery.
  6. 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.

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.