# Sync transactions into accounting platforms with Bank Feeds

By the end of this page you'll have registered a bank feed account against a customer's ledger, pushed a statement of cleared transactions into it with balances that reconcile, and read the statement back to confirm the accounting system accepted it. One integration against the unified [Accounting API](/apis/accounting/reference), three calls, and your users stop exporting CSVs out of your product to import them somewhere else.

_Your platform posts bank feed statements to the Apideck Accounting API, which writes the transactions into whichever ledger your customer uses and returns the accepted statement with its balances._

Bank feeds push transaction data from a fintech, corporate card, or payout platform straight into the customer's accounting system so reconciliation happens automatically against invoices and bills. Apideck exposes this through the [Accounting API](/apis/accounting/reference) with two unified resources, `bank-feed-accounts` and `bank-feed-statements` (both currently in beta). Twelve connectors expose `bank-feed-statements`: Campfire, Moneybird, NetSuite, Odoo, Pennylane, Rillet, Sage Intacct, Sage Intacct (REST), Snelstart, Twinfield, Wave, and Xero. Nine of them can actually post statements today, while Campfire, Odoo, and Wave are read-only. Five connectors, Campfire, Moneybird, NetSuite, Odoo, and Xero, also expose the `bank-feed-accounts` resource, though only NetSuite and Xero support creating a feed account through it; on Campfire, Moneybird, and Odoo it's read-only. The rest post statements against an existing downstream account instead of registering a dedicated feed account first. QuickBooks Online is a further connector currently in development. A working sample app, with institution discovery, simulated bank login, account selection, and a transactions dashboard, lives at [github.com/apideck-samples/bank-feeds-sync](https://github.com/apideck-samples/bank-feeds-sync). The sample currently exercises a subset of these live connectors; the unified resources work the same way against the rest. For a guided, click-through walkthrough of the whole flow, from picking a bank to viewing synced transactions on both the bank and accounting side, try the [Apideck Banking demo](https://banks.apideck.dev).

![The Apideck Banking interactive demo at banks.apideck.dev](/guides/bank-feeds-demo.png)

Bank feeds are distinct from the Accounting API's generic `bank-accounts` and `bank-transactions` resources, which read a connector's own bank ledger rather than push external transaction data into one. If you're looking for a place to *write* bank transactions into an accounting platform for reconciliation, `bank-feed-accounts`/`bank-feed-statements` described here is that resource.

One request shape covers every supported connector, [Vault](/guides/vault) handles the OAuth flow and token refresh per consumer so you never store accounting credentials, and new accounting connectors light up without code changes on your side.

>
> Both bank feed resources are marked `beta` in the live coverage matrix, and coverage is deliberately narrow: 12 connectors accept statements and only 5 expose bank feed accounts. Check the [coverage matrix](/apis/accounting/coverage) for the connector you need before you commit to it.

## Resource mapping

`bank-feed-accounts` is only wired for five connectors; the rest post statements straight against an existing downstream account instead of registering a dedicated feed account first.

| Connector | Feed accounts | Feed statements | Downstream object |
| --- | --- | --- | --- |
| [Xero](/guides/bank-feeds-xero) | ✓ | ✓ | Bank Feed Connection plus Statement (Bank Feeds API) |
| Campfire | ✓ (read) | ✓ (read) | Bank Account plus Bank Transaction batch |
| Moneybird | ✓ (read) | ✓ | - |
| [NetSuite](/guides/bank-feeds-netsuite) | ✓ | ✓ | Apideck Bank Feed Statement/Transaction records, imported via a Format Profile into Match Bank Data |
| Odoo | ✓ (read) | ✓ (read) | `account.journal` (bank) plus `account.bank.statement` |
| Sage Intacct | - | ✓ | Bank Account plus Imported Bank Transactions |
| Sage Intacct (REST) | - | ✓ | Bank Account plus Imported Bank Transactions |
| Pennylane | - | ✓ | - |
| Rillet | - | ✓ | - |
| Snelstart | - | ✓ (create only) | - |
| Twinfield | - | ✓ (create only) | - |
| Wave | - | ✓ (read) | - |
| QuickBooks Online *(in development)* | - | - | Bank Feed Account plus Transaction Batch |

## Walkthrough

The integration has three calls. Register the source account once per consumer and connector, then post statements as transactions clear on your side. Read back posted statements when you need to confirm what the accounting system has accepted.

Each step below shows the raw request first, then the same call through the [`@apideck/unify`](/sdks/node) SDK. Every snippet is verified against `@apideck/unify` v0.46.0.

```ts
import { Apideck } from '@apideck/unify'

const apideck = new Apideck({
  apiKey: process.env.API_KEY!,
  appId: process.env.APP_ID!,
  consumerId: 'cons_01H8X9Y2A3B4C5D6E7F8G9H0J1'
})
```

### 1. Register a bank feed account

A feed account represents the source of funds on your platform, for example a corporate card program or a virtual account. Create it once per consumer and reuse the returned ID for every statement you post against that source.

```http
POST https://unify.apideck.com/accounting/bank-feed-accounts
Authorization: Bearer <APIDECK_API_KEY>
x-apideck-app-id: <APIDECK_APP_ID>
x-apideck-consumer-id: cons_01H8X9Y2A3B4C5D6E7F8G9H0J1
x-apideck-service-id: xero
Content-Type: application/json
```

```json
{
  "bank_account_type": "credit_card",
  "source_account_id": "acme-corporate-card-usd",
  "source_account_number": "4242424242424242",
  "target_account_id": "090",
  "target_account_name": "Acme Corporate Card",
  "currency": "USD",
  "country": "US",
  "feed_status": "pending"
}
```

Several field names differ from what the old shape suggested. There is no `name`, `account_number`, `account_type`, `balance_date`, or `status` on a feed account: the target account is named with `target_account_name`, the source card or wallet number is `source_account_number`, the type is `bank_account_type`, and `feed_status` accepts only `pending` or `rejected`.

Or through the SDK:

```ts
const { createBankFeedAccountResponse } = await apideck.accounting.bankFeedAccounts.create({
  serviceId: 'xero',
  bankFeedAccount: {
    bankAccountType: 'credit_card',
    sourceAccountId: 'acme-corporate-card-usd',
    targetAccountId: '090',
    targetAccountName: 'Acme Corporate Card',
    currency: 'USD',
    country: 'US',
    feedStatus: 'pending'
  }
})

const bankFeedAccountId = createBankFeedAccountResponse?.data?.id
if (!bankFeedAccountId) throw new Error('Could not resolve a bank feed account to post against')
```

Send this to [`POST /accounting/bank-feed-accounts`](/apis/accounting/reference#operation/bankFeedAccountsAdd). The response returns an `id` like `bfa_01H8X9Y2A3B4C5D6E7F8G9H0J1` that you store against the consumer. Most connectors then require the end user to link that feed account to a ledger account inside their accounting platform's UI before statements can be posted. The sample app surfaces this step explicitly so users know where to click.

### 2. Post a statement of cleared transactions

Once the feed account is linked downstream, post statements as transactions settle. A statement is a batch of transactions for a given period, with an opening and closing balance the accounting system uses to validate the math.

```json
{
  "bank_feed_account_id": "bfa_01H8X9Y2A3B4C5D6E7F8G9H0J1",
  "start_date": "2025-04-01T00:00:00.000Z",
  "end_date": "2025-04-07T23:59:59.000Z",
  "start_balance": 12450.75,
  "start_balance_credit_or_debit": "debit",
  "end_balance": 12192.40,
  "end_balance_credit_or_debit": "debit",
  "transactions": [
    {
      "posted_date": "2025-04-02T00:00:00.000Z",
      "description": "AWS, April invoice",
      "amount": 312.18,
      "credit_or_debit": "debit",
      "source_transaction_id": "txn_01H9A2B3C4D5E6F7G8H9J0K1L2",
      "counterparty": "Amazon Web Services",
      "reference": "AWS-2025-04",
      "transaction_type": "payment"
    },
    {
      "posted_date": "2025-04-04T00:00:00.000Z",
      "description": "Refund, Linear annual",
      "amount": 96.00,
      "credit_or_debit": "credit",
      "source_transaction_id": "txn_01H9A2B3C4D5E6F7G8H9J0K1M3",
      "counterparty": "Linear",
      "reference": "LIN-REF-8821",
      "transaction_type": "credit"
    },
    {
      "posted_date": "2025-04-06T00:00:00.000Z",
      "description": "Lunch, client meeting",
      "amount": 42.17,
      "credit_or_debit": "debit",
      "source_transaction_id": "txn_01H9A2B3C4D5E6F7G8H9J0K1N4",
      "counterparty": "Tartine Bakery",
      "reference": "EXP-4417",
      "transaction_type": "payment"
    }
  ]
}
```

Three things there are easy to get wrong. Balances are `start_balance` and `end_balance`, not opening and closing, and each needs its own `start_balance_credit_or_debit` / `end_balance_credit_or_debit` companion. Amounts are always positive: direction lives in `credit_or_debit`, so signing the amount negative double counts it. And the per-transaction key is `source_transaction_id`, not `transaction_id`. There is no `currency` on a statement either; it comes from the feed account.

Or through the SDK:

```ts
await apideck.accounting.bankFeedStatements.create({
  serviceId: 'xero',
  bankFeedStatement: {
    bankFeedAccountId,
    startDate: new Date('2025-04-01T00:00:00.000Z'),
    endDate: new Date('2025-04-07T23:59:59.000Z'),
    startBalance: 12450.75,
    startBalanceCreditOrDebit: 'debit',
    endBalance: 12192.4,
    endBalanceCreditOrDebit: 'debit',
    transactions: [
      {
        postedDate: new Date('2025-04-02T00:00:00.000Z'),
        description: 'AWS, April invoice',
        amount: 312.18,
        creditOrDebit: 'debit',
        sourceTransactionId: 'txn_01H9A2B3C4D5E6F7G8H9J0K1L2',
        counterparty: 'Amazon Web Services',
        reference: 'AWS-2025-04'
      },
      {
        postedDate: new Date('2025-04-04T00:00:00.000Z'),
        description: 'Refund, Linear annual',
        amount: 96,
        creditOrDebit: 'credit',
        sourceTransactionId: 'txn_01H9A2B3C4D5E6F7G8H9J0K1M3',
        counterparty: 'Linear',
        reference: 'LIN-REF-8821'
      }
    ]
  }
})
```

Send this to [`POST /accounting/bank-feed-statements`](/apis/accounting/reference#operation/bankFeedStatementsAdd). Each `transaction_id` should be stable and unique on your side. Most downstream platforms reject duplicates, which makes that ID the safest deduplication key if you ever need to retry.

### 3. Read back posted statements

Use [`GET /accounting/bank-feed-statements`](/apis/accounting/reference#operation/bankFeedStatementsAll) to confirm what the accounting system has accepted, paginate prior periods for backfill UIs, or build the kind of transactions dashboard the sample app ships with.

There is no server-side filter on this resource: all 12 connectors report an empty `supported_filters`, so `filter[bank_feed_account_id]` is silently ignored rather than honoured. Page through with `limit` (max 200) and the `cursor` from `meta.cursors.next`, and match on `bank_feed_account_id` yourself.

```bash
curl -G https://unify.apideck.com/accounting/bank-feed-statements \
  -H "Authorization: Bearer <APIDECK_API_KEY>" \
  -H "x-apideck-app-id: <APIDECK_APP_ID>" \
  -H "x-apideck-consumer-id: cons_01H8X9Y2A3B4C5D6E7F8G9H0J1" \
  -H "x-apideck-service-id: xero" \
  --data-urlencode "limit=200"
```

Or through the SDK, which handles the cursor for you:

```ts
const acceptedStatementIds: string[] = []

for await (const page of await apideck.accounting.bankFeedStatements.list({
  serviceId: 'xero',
  limit: 200
})) {
  for (const statement of page.getBankFeedStatementsResponse?.data ?? []) {
    if (statement.bankFeedAccountId === bankFeedAccountId) {
      acceptedStatementIds.push(statement.id)
    }
  }
}
```

## What actually bites people

Three constraints on the connectors most bank feed products start with, each verified against the live coverage matrix rather than the unified schema.

**Xero.** Statements support `all`, `add`, and `one`, with no `update` and no `delete`, so a posted statement is immutable: to restate a period you post a compensating statement rather than editing the original. The read-back is thinner than the write, too. Xero's statement coverage lists `start_balance`, `start_balance_credit_or_debit`, `end_balance`, and `end_balance_credit_or_debit` but does not list `transactions`, so you get the envelope and its balances back, not the individual lines. Keep your own copy of what you sent. Xero feed accounts also have no `update` operation (`all`, `add`, `one`, `delete` only), so correcting one means deleting it and creating a replacement.

**NetSuite.** NetSuite has the fullest coverage of any connector here, and it is the only one with all five operations on `bank-feed-accounts`. It is also the only one that needs work inside the customer's account before a single statement lands: the Apideck bundle, the Bank Statement Parsers SuiteApp, a Format Profile with account linking, and the RESTlet script and deploy IDs saved into Vault. The unified calls on this page are correct for NetSuite, but they are not sufficient on their own. Walk the administrator through [NetSuite Bank Feeds](/guides/bank-feeds-netsuite) first. Note also that NetSuite statement coverage does not include `transaction_type`, so that field is dropped there.

**Sage Intacct.** Sage Intacct is not one of the five connectors that expose `bank-feed-accounts` at all, so step 1 does not apply: the bank account has to exist in Intacct already and you reference it by ID on the statement. Statements support `all`, `add`, `one`, and `delete` but not `update`, and neither Intacct connector maps `start_balance` or `end_balance`, so the balance reconciliation Xero and NetSuite enforce is not something you can read back from Intacct. The two Intacct connectors also differ: the legacy `sage-intacct` connector maps `start_date` but not `end_date`, while `sage-intacct-rest` maps both.

## Connector-specific behavior

The table below covers every live connector Apideck supports for Bank Feeds. The sample app at [bank-feeds.vercel.app](https://bank-feeds.vercel.app) currently demonstrates a subset of them; the unified resources behave the same way against the others.

| Connector | Notes |
| --- | --- |
| [Xero](#xero) | Feed account must be linked to a Xero bank account by the user before statements are accepted. Opening and closing balances must reconcile against the previous statement's closing balance. |
| Campfire | Read-only today: `bank-feed-accounts` and `bank-feed-statements` are wired for listing, writes aren't yet supported downstream. |
| Moneybird | `bank-feed-accounts` is list-only (no create); `bank-feed-statements` supports create and read. |
| [NetSuite](#netsuite) | Requires an admin to install a SuiteBundle and configure a Format Profile before any statement is accepted, unlike most of the other live connectors, which need no more than linking the feed account. |
| Odoo | Read-only for both resources today; statements are surfaced but not yet postable through the unified endpoint. |
| Sage Intacct | Statements-only. No dedicated `bank-feed-accounts` resource, standard mapping otherwise. |
| Sage Intacct (REST) | Statements-only, same model as legacy Sage Intacct. |
| Pennylane | Statements-only. Standard mapping. |
| Rillet | Statements-only, but with full create/read/update/delete support. |
| Snelstart | Statements-only and create-only; posted statements aren't currently readable back through the unified endpoint. |
| Twinfield | Statements-only and create-only; posted statements aren't currently readable back through the unified endpoint. |
| Wave | Statements-only and read-only; no create support yet. |
| [QuickBooks Online](#quickbooks-online) *(in development)* | Not yet generally available. Once it ships, expect a QuickBooks Online tier with bank feeds enabled on the customer's side, and the feed account's currency to match the connected QuickBooks account currency. |

### Xero

Xero validates the closing balance of each statement against the opening balance of the next one. If your platform produces gaps or restates a prior period, post a correcting statement rather than editing the original. The sample app demonstrates this by showing the running balance reported back from `bankFeedStatementsAll` next to your own ledger.

For the full walkthrough, including how end users link the feed account inside Xero, see [How to Integrate Xero Bank Feeds Using Apideck](/guides/bank-feeds-xero).

### NetSuite

NetSuite doesn't expose a bank feeds surface you can post to directly. Apideck ships a SuiteBundle (custom records, a RESTlet, and a connectivity plug-in) that an admin installs once per account, then wires up through a Format Profile under **Setup → Accounting → Financial Institution**. Once that's in place, the same `bank-feed-statements` endpoint delivers transactions into **Banking → Match Bank Data** on the next import cycle.

See [How to Integrate NetSuite Bank Feeds Using Apideck](/guides/bank-feeds-netsuite) for the full setup, including required role permissions and the Format Profile field mapping.

### QuickBooks Online

>
> QuickBooks Online Bank Feeds is currently in development and not yet generally available. The details below reflect the current build and may change before launch. Reach out to your Apideck contact if you want early access.

Once available, QuickBooks Online will reject statements posted to a feed account whose currency doesn't match the linked QuickBooks bank account, so plan on creating a separate `bank-feed-account` per currency you support.

## FAQ

**Can I use bank feeds to pull my customers' banking data into my platform?**
No. Bank feeds are one-directional: you push transaction data your platform already holds *into* the accounting system for reconciliation. If you need to pull raw transaction data from a customer's bank, that's a different problem, aggregators like Plaid or Yodlee, not a unified bank feed.

**I already sync invoices, bills, and expenses through the Accounting API. Why do I also need bank feeds?**
The Accounting API keeps records like invoices and bills in sync, but bank feeds deliver the underlying cash movement, the data accountants treat as source of truth when reconciling. Posting a bank feed alongside your existing accounting automation gives customers automated matching instead of manually reconciling bank statements by hand.

**Why integrate through Apideck instead of going directly to each accounting platform's bank feed API?**
Several of the underlying APIs are gated. Xero's Bank Feeds API, for example, is closed to companies without an established financial-institution partnership (see the [Xero bank feeds guide](/guides/bank-feeds-xero)), and NetSuite requires installing a bundle and configuring a Format Profile before it will accept anything (see the [NetSuite bank feeds guide](/guides/bank-feeds-netsuite)). Apideck normalizes the request shape across all twelve live connectors (with QuickBooks Online in development) and handles the OAuth and token lifecycle through Vault, so one integration replaces separate certifications with every platform.

**Do my customers still need to do anything inside their accounting platform?**
Usually yes, once. Most connectors require the end user to link the bank feed account you created to an existing ledger or bank account inside their accounting platform's UI before statements are accepted. That's the linking step the sample app surfaces explicitly, and it's typically a one-time action per account.

**Can my company appear as a bank or financial institution inside my customers' accounting software even though we're not a licensed bank?**
Yes, being a licensed bank isn't the requirement. Bank feeds are built for fintechs, neobanks, corporate card issuers, and payout platforms that hold source-of-truth transaction data on behalf of their customers, not only chartered banks. Posting accurate transactions and balances is necessary but not sufficient on its own: several connectors have their own certification or setup gate before they'll accept a feed at all, for example Xero requires an established financial-institution partnership and NetSuite requires installing a SuiteBundle (see [Connector-specific behavior](#connector-specific-behavior) above for what each connector actually requires).

**What happens if my statements don't reconcile or I need to correct a past period?**
It depends on the connector, most validate that opening and closing balances tie out across consecutive statements (see [Connector-specific behavior](#connector-specific-behavior) above). If you posted the wrong figures, post a new correcting statement for that period rather than trying to edit or delete the original, then read the account back with `GET /accounting/bank-feed-statements` to confirm what was actually accepted.

## Visit the demo

The Apideck Banking demo runs this flow end to end on synthetic data: pick a bank, sign in, choose accounts, sync via the API, then reconcile and watch the same transactions land in the accounting view. A second track underwrites a credit facility from the borrower's live ledger, and both cover the 12 beta accounting connectors listed above.

## Next steps

- Connector-specific deep dives: [Xero Bank Feeds](/guides/bank-feeds-xero) and [NetSuite Bank Feeds](/guides/bank-feeds-netsuite)
- Reference: [Bank Feed Accounts](/apis/accounting/reference#operation/bankFeedAccountsAdd) and [Bank Feed Statements](/apis/accounting/reference#operation/bankFeedStatementsAdd)
- Worked example: [apideck-samples/bank-feeds-sync](https://github.com/apideck-samples/bank-feeds-sync), a Next.js app with a simulated bank login that walks through institution discovery, account selection, and posting sample statements through the real API against a subset of the live connectors
- Interactive demo: [Apideck Banking](https://banks.apideck.dev), a guided tour from picking a bank through to the synced view on both the bank and accounting side
- Related accounting use cases: [Expense Management Integration](/guides/expense-management-integration) and [Mark Invoices as Paid](/guides/mark-invoices-as-paid)
