# Build a bank reconciliation agent

Bank reconciliation lines up every line on a bank statement against the ledger activity for the same account, then explains anything that doesn't match. This guide builds an agent on top of the [Accounting API](/apis/accounting/reference) that pulls bank-side movement (bank feed statements, or activity booked to the bank ledger account), pulls ledger-side movement (payments, bill payments, journal entries and general ledger transactions), proposes matches with a confidence score, and posts the fixes it is sure about while handing the rest to a human reviewer.

## One integration for every ledger

A reconciliation agent needs the same four things from every accounting system: a list of bank-type accounts, the statement lines on each account, the ledger activity booked against each account, and a way to post the small corrections that make a reconciliation balance. Each accounting platform models those differently. Through the unified API the agent reads and writes one shape.

- No per-connector code for enumerating bank accounts, paging transactions or writing journal entries.
- One payment allocation model when the fix is "mark this invoice paid", and one journal entry model when the fix is a bank fee or interest line.
- The same proposal output regardless of whether the downstream is QuickBooks, Xero, NetSuite or Sage Intacct.
- One place to add a connector-specific guard when the coverage matrix says a resource isn't writable.

## Resource mapping

The reconciliation inputs and outputs map to different downstream objects per connector. The agent reads bank-side activity from one source and ledger-side activity from the standard accounting resources.

| Connector | Bank-side source | Ledger-side sources | Fix-posting targets |
| --- | --- | --- | --- |
| QuickBooks | `Account` (ledger account `type: bank`). No bank feed statements in the unified API | `Payment`, `BillPayment`, `JournalEntry` | `Payment`, `BillPayment`, `JournalEntry` |
| Xero | `Account` (ledger account `type: bank`), bank feed statements | `Payment`, `JournalEntry`, general ledger transactions | `Payment`, `JournalEntry` |
| NetSuite | Bank `Account`, bank feed statements | `CustomerPayment`, `VendorPayment`, `JournalEntry` | `CustomerPayment`, `VendorPayment`, `JournalEntry` |
| Sage Intacct | Checking `Account`, bank feed statements | `ARPayment`, `APPayment`, `JournalEntry` | `ARPayment`, `APPayment`, `JournalEntry` |
| Exact Online | Bank `GLAccount`, bank feed statements | `Payment`, `BillPayment`, `JournalEntry` | `Payment`, `JournalEntry` |

## Walkthrough

The agent runs on a schedule per connected consumer, scoped to one bank account and a date window (typically the last closed statement period). All calls below include the standard Apideck headers; later snippets omit them for brevity.

### 1. Enumerate bank-type ledger accounts

Start by listing the ledger accounts the consumer treats as bank or credit card accounts. The agent uses this list to pick a target account and to resolve the account `id` later when posting fixes. Send this to [`GET /accounting/ledger-accounts`](/apis/accounting/reference#operation/ledgerAccountsAll), MCP tool `accounting-ledger-accounts-all`.

`LedgerAccountsFilter` has no `type` filter. It accepts `classification`, `status`, `name`, `updated_since` and `subsidiary_id`. Request active accounts and keep the rows whose `type` is `bank` or `credit_card` client-side. Filtering on `classification=asset` alone would drop credit card accounts, which are usually liabilities.

```http
GET /accounting/ledger-accounts?filter[status]=active HTTP/1.1
Host: unify.apideck.com
Authorization: Bearer {API_KEY}
x-apideck-app-id: {APP_ID}
x-apideck-consumer-id: {CONSUMER_ID}
x-apideck-service-id: xero
```

A single row from the response:

```json
{
  "id": "acct_01H8X9Y2A3BQMRS4VNEHW4F2CK",
  "name": "Operating Checking 4421",
  "nominal_code": "1050",
  "classification": "asset",
  "type": "bank",
  "currency": "USD",
  "current_balance": 184203.47,
  "last_reconciliation_date": "2025-08-31"
}
```

Where the connector returns `last_reconciliation_date`, use it as the start of the date window.

### 2. Pull bank-side activity

Where the connector supports bank feeds, read the statements posted to the account. First list [`GET /accounting/bank-feed-accounts`](/apis/accounting/reference#operation/bankFeedAccountsAll) (MCP tool `accounting-bank-feed-accounts-all`) and find the feed account whose `target_account_id` is the ledger account from step 1. Then send its `id` to [`GET /accounting/bank-feed-statements`](/apis/accounting/reference#operation/bankFeedStatementsAll), MCP tool `accounting-bank-feed-statements-all`.

`bank_feed_account_id` is the only filter on statements, so drop statements outside the reconciliation window client-side on `start_date` and `end_date`.

```http
GET /accounting/bank-feed-statements?filter[bank_feed_account_id]=bfa_01HPQR6ZXK7F2M9GN3RTVC9M7B HTTP/1.1
Host: unify.apideck.com
x-apideck-service-id: xero
```

```json
{
  "data": [
    {
      "id": "bfs_01HPQR6ZXK7F2M9GN3RTVC9M7B",
      "bank_feed_account_id": "bfa_01HPQR6ZXK7F2M9GN3RTVC9M7B",
      "status": "success",
      "start_date": "2025-09-01T00:00:00Z",
      "end_date": "2025-09-30T23:59:59Z",
      "start_balance": 185408.41,
      "start_balance_credit_or_debit": "credit",
      "end_balance": 184203.47,
      "end_balance_credit_or_debit": "credit",
      "transactions": [
        {
          "source_transaction_id": "txn_8821",
          "posted_date": "2025-09-04T00:00:00Z",
          "amount": 1240.00,
          "credit_or_debit": "debit",
          "transaction_type": "payment",
          "counterparty": "Acme Supplies LLC",
          "reference": "INV-10423",
          "description": "ACH DEBIT Acme Supplies INV-10423"
        },
        {
          "source_transaction_id": "txn_8822",
          "posted_date": "2025-09-05T00:00:00Z",
          "amount": 49.99,
          "credit_or_debit": "credit",
          "transaction_type": "deposit",
          "counterparty": "Northwind Trading",
          "reference": "123456",
          "description": "DEPOSIT ref 123456"
        },
        {
          "source_transaction_id": "txn_8830",
          "posted_date": "2025-09-30T00:00:00Z",
          "amount": 14.95,
          "credit_or_debit": "debit",
          "transaction_type": "other",
          "description": "MONTHLY SERVICE FEE"
        }
      ]
    }
  ]
}
```

Amounts on statement transactions are unsigned; `credit_or_debit` gives the direction. Convert `debit` lines to negative amounts when normalizing.

When the connector does not expose bank feed statements, substitute whatever the consumer has booked to the bank account in the ledger itself. For QuickBooks that is the payments, bill payments and journal entries posted to the bank ledger account. The agent should treat anything already booked to the bank account as bank-side input for matching.

### 3. Pull ledger-side activity

Pull payments, bill payments, journal entries and general ledger transactions for the same account and date range. None of these endpoints accepts an account filter, so filter to the target account client-side:

- Payments and bill payments accept `filter[updated_since]` but no date range. Keep rows where `account.id` matches and `transaction_date` falls in the window. Skip rows with `reconciled: true`.
- Journal entries accept `filter[start_date]` and `filter[end_date]` on the posting date. Connectors without date-range support reject these with `UnsupportedFiltersError`; fall back to `filter[updated_since]` there. Keep entries with a line whose `ledger_account.id` matches.
- General ledger transactions accept `filter[updated_since]`, `filter[source_type]` and `filter[source_id]`. Keep transactions with a line whose `ledger_account.id` matches. Few connectors cover this resource (Xero, FreeAgent and Twinfield among them), so treat it as optional.

```http
GET /accounting/payments?filter[updated_since]=2025-09-01T00:00:00Z HTTP/1.1
Host: unify.apideck.com
x-apideck-service-id: xero
```

MCP tool names: `accounting-payments-all` ([`GET /accounting/payments`](/apis/accounting/reference#operation/paymentsAll)), `accounting-bill-payments-all` ([`GET /accounting/bill-payments`](/apis/accounting/reference#operation/billPaymentsAll)), `accounting-journal-entries-all` ([`GET /accounting/journal-entries`](/apis/accounting/reference#operation/journalEntriesAll)), and `accounting-general-ledger-transactions-all` ([`GET /accounting/general-ledger-transactions`](/apis/accounting/reference#operation/generalLedgerTransactionsAll)).

Normalize every ledger row into the same shape as a bank line so the matcher can compare like with like:

```json
{
  "source": "bill_payment",
  "ledger_id": "bpay_01HPT4ABQNFCW65V7Q92RJ3XKY",
  "transaction_date": "2025-09-04",
  "amount": -1240.00,
  "currency": "USD",
  "counterparty": "Acme Supplies LLC",
  "reference": "INV-10423",
  "account_id": "acct_01H8X9Y2A3BQMRS4VNEHW4F2CK"
}
```

### 4. Run the matching strategy

The agent runs each bank line against the normalized ledger pool in cascading passes. Each pass only looks at lines that have not been matched yet, and each pass attaches a confidence score to its proposal.

| Pass | Rule | Confidence |
| --- | --- | --- |
| 1 | Bank amount equals ledger amount to the cent, and `posted_date` equals `transaction_date`. | `0.98` |
| 2 | Same amount, `posted_date` within 3 business days of `transaction_date`. | `0.90` |
| 3 | Same amount within a 7-day window, plus an identical `reference`, token overlap on `counterparty`, or a bank `description` that contains the ledger reference. | `0.75` |
| 4 | One bank line against a sum of ledger lines (payment runs, batched deposits), or one ledger entry against several statement lines (card settlement sweeps). Only propose a group when the sum ties exactly and the dates fall within a 5-day window. | `0.65` |
| 5 | Residual: everything that falls through, routed straight to the review queue. | `0.0` |

Store the proposal with the inputs, the rule that fired and the score. The score decides whether the agent posts the fix automatically or asks a human.

```json
{
  "proposal_id": "prop_01HPT4C9W6Q3D5MA1F8KH4YG7T",
  "bank_line_id": "txn_8821",
  "ledger_matches": [{ "source": "bill_payment", "ledger_id": "bpay_01HPT4ABQNFCW65V7Q92RJ3XKY" }],
  "rule": "exact_amount_exact_date",
  "confidence": 0.98,
  "proposed_action": "mark_matched",
  "requires_review": false
}
```

### 5. Post the agreed fixes

Three fix shapes cover most reconciliation adjustments: a payment that allocates against an open invoice or bill, a journal entry for bank fees and interest, and a flag that leaves the item for a human. See the [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid) guide for the end-to-end payment allocation flow.

#### Allocate a payment

When the bank line represents a customer payment against an open invoice, post it through [`POST /accounting/payments`](/apis/accounting/reference#operation/paymentsAdd), MCP tool `accounting-payments-add`. The accounting system updates the invoice status from the allocation.

```json
{
  "transaction_date": "2025-09-05T00:00:00Z",
  "currency": "USD",
  "total_amount": 49.99,
  "payment_method": "ACH",
  "reference": "123456",
  "type": "accounts_receivable",
  "account": {
    "id": "acct_01H8X9Y2A3BQMRS4VNEHW4F2CK",
    "name": "Operating Checking 4421"
  },
  "customer": {
    "id": "cust_01HPT4D1J2R0A3B4C5D6E7F8G9",
    "display_name": "Northwind Trading"
  },
  "allocations": [
    {
      "id": "inv_01HPT4E8M9N0P1Q2R3S4T5U6V7",
      "type": "invoice",
      "amount": 49.99
    }
  ],
  "note": "Reconciled from bank statement line txn_8822"
}
```

Bill payments follow the same shape, with `supplier` instead of `customer` and `type: "bill"` allocations, against [`POST /accounting/bill-payments`](/apis/accounting/reference#operation/billPaymentsAdd), MCP tool `accounting-bill-payments-add`.

#### Journal entry for fees and interest

When the bank line is a bank fee, interest earned, or a currency adjustment that has no counterpart in the ledger, post a journal entry. Send this to [`POST /accounting/journal-entries`](/apis/accounting/reference#operation/journalEntriesAdd), MCP tool `accounting-journal-entries-add`.

```json
{
  "title": "Bank service fee September 2025",
  "posted_at": "2025-09-30T00:00:00Z",
  "currency": "USD",
  "memo": "Reconciled from bank statement line txn_8830",
  "line_items": [
    {
      "ledger_account": { "id": "acct_01HPT4F7K8L9M0N1O2P3Q4R5S6" },
      "description": "Monthly service fee",
      "type": "debit",
      "total_amount": 14.95
    },
    {
      "ledger_account": { "id": "acct_01H8X9Y2A3BQMRS4VNEHW4F2CK" },
      "description": "Operating Checking 4421",
      "type": "credit",
      "total_amount": 14.95
    }
  ]
}
```

#### Flag for human review

Everything that failed to match or scored below the auto-post threshold goes into a review queue the agent owns. Store the bank line, the top candidate matches with their scores, and the rule that almost fired. Nothing is written to the accounting system until a human either confirms a candidate (triggering one of the two post actions above) or supplies a counterparty, customer or supplier that the agent then uses to re-run the match.

Before posting any fix, check the ledger for an existing payment or journal entry carrying the same statement line reference in `note` or `memo`, so a retried run does not post the same fix twice. For journal entries, also send an [`Idempotency-Key` header](/guides/idempotency-keys) derived from the statement line's `source_transaction_id`: a retry with the same key and body within 24 hours (6 minutes on Xero) posts the entry at most once. Payments do not accept the header, so they rely on the lookup.

### 6. Confidence thresholds and the review queue

A sensible default is to auto-post at `confidence >= 0.90`, post as `status: draft` and surface for approval between `0.75` and `0.90` on connectors that accept draft payments and journal entries, and always queue for review below `0.75`. Tune per consumer.

## How this differs from bank feeds

The bank feeds use case (covered in [Xero Bank Feeds Integration](/guides/bank-feeds-xero)) pushes transaction data from a card program or neobank into the customer's accounting system so a human can reconcile. This guide reads what is already there, on both sides, and proposes the reconciliation itself.

The unified model exposes some reconciliation state, such as `reconciled` on payments and bill payments and `last_reconciliation_date` on ledger accounts, but there is no endpoint to tick statement lines or lock a statement period. The agent therefore produces a proposed reconciliation. It writes the payment allocations and the fee journal entries, surfaces its proposals and residuals, and the consumer or their bookkeeper performs the formal reconcile action inside the accounting UI.

## Connector-specific behavior

The matrix below is scoped to the three resources the agent writes to (`payments`, `bill-payments` and `journal-entries`) plus the bank-side reads. Connectors where a fix resource is read-only or absent should degrade to a review-only mode for that fix type. The notes reflect the coverage matrix at the time of writing; verify the live matrix before enabling a connector for auto-posting.

| Connector | Notes |
| --- | --- |
| `access-financials` | Payments can be created. No `bill-payments` or `journal-entries` coverage, so AP settlements and fee adjustments route to review. |
| `acumatica` | Payments and journal entries are writable. No `bill-payments` coverage. Settle vendor matches through journal entries or route to review. |
| `banqup` | No coverage for payments, bill payments or journal entries. Propose-only mode. |
| `campfire` | Bank feed statements are readable. Payments can be created but not listed, and there is no `bill-payments` coverage. Journal entries are writable; prefer them for fix posting. |
| `clearbooks-uk` | No coverage for payments, bill payments or journal entries. Propose-only mode; a human posts fixes in the Clear Books UI. |
| `digits` | Journal entries are read-only; no payments or bill payments coverage. Propose-only mode. |
| `dualentry` | All three fix shapes are writable. No bank feed statements, so use activity booked to the bank ledger account as bank-side input. |
| `exact-online` | All three fix shapes are writable. Bank feed statements are readable. |
| `exact-online-nl` | Same as `exact-online`. |
| `exact-online-uk` | Same as `exact-online`. |
| `freeagent` | All three fix shapes are writable. Bank feed statements and general ledger transactions are readable. |
| `freshbooks` | Journal entries are read-only. Post payment allocations as normal; route bank fee and interest adjustments to review. |
| `intuit-enterprise-suite` | All three fix shapes are writable. No bank feed statements. |
| `kashflow` | Payments and journal entries are read-only; no `bill-payments` coverage. Propose-only mode. |
| `microsoft-dynamics-365-business-central` | Payments are read-only; bill payments and journal entries are writable. Settle AR matches via journal entries or route to review. |
| `moneybird` | Payments and bill payments can be created but not listed, so ledger-side settlement data has to come from journal entries. Journal entries are writable and bank feed statements are readable. |
| `mrisoftware` | Journal entries are the only writable fix shape. Post every reconciliation fix as a balanced journal entry. |
| `myob` | All three fix shapes are writable. No bank feed statements. |
| `myob-acumatica` | Payments and journal entries are writable; no `bill-payments` coverage. Settle AP matches through journal entries or route to review. |
| `netsuite` | All three fix shapes are writable, and bank feed statements are readable. `payments` map to customer payments; post vendor settlements through `bill-payments`. |
| `odoo` | Payments, bill payments and journal entries can be created but not updated. Bank feed statements are read-only. |
| `pennylane` | No payments or bill payments coverage; journal entries are writable. Bank feed statements map to Pennylane transactions. Post all fixes as journal entries. |
| `procountor-fi` | Payments are read-only, no `bill-payments` coverage, journal entries are writable. Post bank fees and interest as journal entries; route payment matches to review. |
| `quickbooks` | All three fix shapes are writable. Bank feed statements are not available on this connector; Intuit bank feeds are a separate `intuit-bank-feeds` connector. |
| `rillet` | Payments can be created and listed; bill payments can be created but not read. Journal entries and bank feed statements are fully supported. Prefer journal entries for vendor-side adjustments. |
| `sage-business-cloud-accounting` | Journal entries are read-only; payments and bill payments are writable. Post allocations as normal; route fee and interest journal entries to review. |
| `sage-intacct` | All three fix shapes are writable. Bank feed statements are readable. |
| `sage-intacct-rest` | Same as `sage-intacct`, except payments cannot be updated after creation. |
| `stripe` | Payments are read-only; no bill payments or journal entries. Treat as a bank-side source only (card settlements, refunds). All postings happen on the paired accounting connector. |
| `visma-netvisor` | Payments can be created, journal entries are read-only, no `bill-payments` coverage. Payment allocations post normally; everything else routes to review. |
| `wave` | No coverage for payments, bill payments or journal entries. Bank feed statements can be listed. Propose-only mode. |
| `workday` | Bill payments are read-only; payments and journal entries are writable. Route vendor-side matches to review. |
| `xero` | All three fix shapes are writable. Bank feed statements and general ledger transactions are readable. |
| `yuki` | No payments or bill payments coverage; journal entries can be created. Post all fixes as journal entries. |
| `zoho-books` | All three fix shapes are writable. Bank feed statements are readable. |

### `stripe`

Stripe exposes payments through the Accounting API as a read-only source and is not a general ledger. In a reconciliation workflow it acts as a bank-side source: pull its payments, match them against the ledger-side activity on the paired accounting connector (the one the consumer books into), and post all fixes to that paired connector.

### `mrisoftware`, `pennylane`, `yuki`

These connectors have journal entries as their only writable fix shape among the three the agent uses. Model every reconciliation fix (customer settlement, vendor settlement, bank fee) as a balanced journal entry rather than switching posting paths based on fix type.

### `banqup`, `clearbooks-uk`, `digits`, `kashflow`, `wave`

These connectors do not expose writable payments, bill payments or journal entries. Run them in propose-only mode: output the proposals and the review queue, and let the consumer apply the fixes in the native UI.

## Next steps

- [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid) for the payment allocation shape the agent uses as its primary fix.
- [Xero Bank Feeds Integration](/guides/bank-feeds-xero) for the opposite flow: pushing transactions into Xero from a card program.
- [Accounting API reference](/apis/accounting/reference) for the full list of endpoints and field-level schemas on `payments`, `bill-payments`, `journal-entries` and `general-ledger-transactions`.
