Back to blog

API-First Banking Infrastructure: Integration Patterns for Cash-Flow Features

Three production integration patterns for embedding real-time transaction classification into a banking app. Trade-offs and when to use each.

API-First Banking Infrastructure: Integration Patterns for Cash-Flow Features

Choosing an integration pattern is an architectural commitment

When a banking app team decides to integrate a transaction classification API, the first technical question isn't "which API?" — it's "where in our pipeline does classification happen, and in which timing model?" The answer shapes your data model, your state management, and your UI behavior in ways that are hard to change after shipping to production.

There are three production-viable patterns for integrating real-time transaction classification into a banking app. Each has genuine trade-offs, and the right choice depends on your app's current architecture, your latency tolerance, and how much new infrastructure complexity you're willing to take on in exchange for a simpler integration path. This article describes all three without pretending one is always correct.

Pattern 1: Synchronous enrichment in the transaction fetch path

In this pattern, when your app fetches transactions from your open banking provider (Plaid webhook received → your backend fetches the transaction batch → processes before storing), classification happens synchronously before the transactions are written to your database. The app never stores uncorrected categories.

// Plaid webhook handler (simplified)
app.post('/webhooks/plaid', async (req, res) => {
  const { new_transactions } = req.body;
  const raw = await plaidClient.transactionsGet({ ... });

  // POST to Spendaq classification before writing to DB
  const { classified } = await spendaqClient.classify({
    transactions: raw.transactions,
    account_context: { industry: 'construction', ... }
  });

  await db.transactions.insertMany(classified);
  res.sendStatus(200);
});

Advantages: Architecturally clean — the downstream database and all features built on it only ever see corrected data. No dual-state management (raw vs. corrected). No UI flicker. No webhook delivery system to build or maintain.

Disadvantages: The Plaid webhook handler latency now includes the classification API call. If classification takes 150ms and Plaid expects the webhook acknowledged within a tight window, this can be a timing concern. The standard mitigation is to acknowledge the Plaid webhook immediately, queue the transaction IDs, and classify asynchronously before writing — which is a small but real step toward Pattern 2.

Best for: New integrations where you control the data model from the start. Neobanks that haven't shipped yet and can design the pipeline correctly from day one. Teams that want the simplest possible architecture and don't have existing raw-category data to migrate.

Pattern 2: Async webhook delivery for event-driven architectures

In this pattern, your app writes raw transactions from the open banking feed immediately (fast acknowledge, no latency overhead), and classification results are delivered later via webhook. Your data model holds both the raw category and the corrected category, updating the corrected field when the webhook arrives.

// Your data model
{
  "transaction_id": "txn_8a3f",
  "merchant": "SYSCO CORP 4401",
  "amount": -1842.00,
  "raw_category": "Shopping",
  "corrected_category": null,  // populated when webhook arrives
  "classification_status": "pending"
}

// Spendaq webhook delivery
POST /your-endpoint/spendaq-webhook
{
  "event": "classification.completed",
  "transaction_id": "txn_8a3f",
  "corrected_category": "Food and Beverage COGS",
  "confidence_score": 0.93
}

Advantages: Zero latency impact on your primary transaction fetch path. Works well with existing architectures that already use event-driven state updates. Allows progressive enhancement of existing data — useful if you have a backlog of raw-category transactions you want to reclassify without pipeline changes.

Disadvantages: Requires maintaining dual state (raw + corrected). UI must handle pending states — showing raw category until corrected category arrives, which produces flicker if the user is watching the transaction feed in real time. Webhook delivery introduces reliability concerns: what happens if your webhook endpoint is down when the classification result arrives? Idempotency and retry handling become your responsibility.

Best for: Teams that have already shipped with an existing open banking pipeline and cannot redesign the synchronous path without significant migration work. Apps where transaction feeds update in the background and users are unlikely to see the pending-to-corrected transition in real time.

Pattern 3: Batch reconciliation on a scheduled cycle

In this pattern, your app runs a nightly or periodic batch job that takes all transactions from the previous period with missing or low-confidence categories, POSTs them to the classification API, and updates the database with corrected results. Classification is decoupled from the real-time transaction pipeline entirely.

This is the lowest-complexity integration and the most appropriate starting point for teams that already have significant transaction history they want to reclassify, or for teams whose SMB customers primarily use the app for retrospective reporting rather than real-time monitoring.

Advantages: No changes to the real-time transaction pipeline. Easiest to build and maintain. Works on historical data. Good for compliance reporting workflows where accuracy matters more than timeliness.

Disadvantages: Cash-flow forecasts and dashboards show wrong categories until the next batch run — which can be 12-24 hours for a nightly job. Not viable for any real-time cash-flow monitoring feature. Batch sizes at high account volumes can produce long processing windows.

Best for: Retrospective accounting export features. Historical data correction projects. Teams validating classification quality before committing to a synchronous integration. Apps primarily serving businesses that review their finances weekly or monthly rather than daily.

The state management problem in async patterns

The hardest part of Pattern 2 isn't the webhook endpoint — it's deciding how to represent classification state in your UI. If a user opens the expense breakdown dashboard while 40% of this month's transactions are still in classification_status: pending, what do you show? The options are: show the raw category with a visual indicator that refinement is in progress; show an aggregated total that excludes uncategorized items (which understates the total); or hold the dashboard render until classification completes (which adds latency to the UX).

We're not saying any of these choices is wrong — they're trade-offs with different UX implications depending on your user's context. A business owner who is reviewing transactions at 9pm isn't tolerant of "pending" states on every other row. A product that waits until all transactions are classified before showing the dashboard respects that, but creates a different friction at first load.

This is the architectural reason to prefer Pattern 1 when you have the option: by solving the timing at the data pipeline layer, you avoid having to solve it at the UI layer. A database that only ever holds corrected categories has no pending state problem. The user always sees accurate data.

Migration considerations for existing deployments

If your app has been running on raw open banking categories for months and you're integrating reclassification for the first time, you have a historical data problem alongside the forward pipeline problem. Existing dashboard data, forecast baselines, and account history all reflect the old (wrong) categories. Users who have been looking at certain numbers for months will notice when the numbers change.

The cleanest migration approach: run a backfill batch job using Pattern 3 to reclassify all historical transactions, then switch the forward pipeline to Pattern 1. Before shipping the backfilled categories to production, validate the change set with a sample — review which categories changed and whether the changes are improvements or regressions. A reclassification pass that corrects 95% of misclassifications but introduces 5% new errors on previously correct transactions is still net positive, but those new errors will be visible and should be understood before shipping.

On the communication side: if your app shows aggregate numbers (monthly expenses by category, 90-day cash-flow trend), those numbers will change after reclassification. Some users will notice and ask what happened. Having a clear, honest explanation — "we improved the accuracy of transaction categorization and your historical data has been updated to reflect the corrections" — is the right answer, and most users who were confused by wrong categories will appreciate the fix rather than being disturbed by the change.