---
title: "How to Reconcile Customer Payments (AR) with Unified's Accounting API"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_reconcile_customer_payments_ar_with_unified_accounting_api-icon.png
date: 2026-01-19T21:56:00.000Z
tag: Product
description: "Accounts Receivable reconciliation is often implemented in the wrong place."
url: "https://unified.to/blog/how_to_reconcile_customer_payments_ar_with_unified_accounting_api"
---

# How to Reconcile Customer Payments (AR) with Unified's Accounting API
------
_January 19, 2026_

Accounts Receivable reconciliation is often implemented in the wrong place.


Teams start with payment events—charges, refunds, payouts—and try to infer AR state from them. That works in a single-provider setup, but it breaks as soon as you support multiple accounting systems. Payment processors report activity. Accounting systems record obligation and settlement. Those are not the same thing.


For PMs and backend teams, this mismatch creates immediate ambiguity:

- Is AR driven by what was charged, or what the ledger recorded?
- How do partial payments, credits, and write-offs actually surface?
- Which numbers should users trust when systems disagree?

When reconciliation logic is built on payment events alone, edge cases accumulate quickly. Credits don't line up. Invoice balances drift. Collections workflows lose credibility.


Unified's [Accounting API](/accounting) is designed to anchor AR reconciliation where it belongs: in the general ledger. In Unified Accounting, **invoices define what is owed**, and **transactions define what posted**. Customer payments are not a separate object—they are ledger entries that must be interpreted in context.


This guide shows how to reconcile AR defensively using that model—combining invoices and transactions, matching payments best-effort using documented fields only, and treating invoice balances as the final authority—so your AR logic remains correct across accounting providers.


## Why AR reconciliation lives in Accounting (not Payments)


Unified exposes two different surfaces that mention 'payments':

- **[Payments API](/payment)**: payment-processor events (e.g., Stripe/Square charges, refunds, payouts).
- **Accounting API**: the **general ledger** view—what the accounting system recorded.

**AR reconciliation belongs to Accounting**, because AR is a ledger concept. In Unified Accounting:

- **Invoices** represent **what's owed**.
- **Transactions** represent **what happened** (including customer payments).
- **Accounts** classify those transactions (e.g., AR vs bank).

There is **no standalone 'Payment' object** in Accounting. Customer payments surface as **AccountingTransaction** records.


## What this guide covers

- Listing invoices and transactions with [pagination](/docs/reference/pagination) and incremental sync
- Understanding how customer payments appear as transactions
- Matching payments to invoices (best-effort, doc-accurate)
- Computing open AR, paid AR, partials, and credits
- Validating results against account balances and reports (optional)

## Prerequisites

- Node.js 18+
- A Unified workspace
- An Accounting integration authorized for a customer
- A valid `connectionId`
- Unified API key

## Step 1: Initialize the SDK


```typescript
import { UnifiedTo } from '@unified-api/typescript-sdk';

const sdk = new UnifiedTo({
  security: {
    jwt: process.env.UNIFIED_API_KEY!,
  },
});
```


All Accounting API calls are scoped by `connectionId`, which represents a single authorized customer integration.


## Step 2: Understand the two source objects


### Invoices = AR state


From the `AccountingInvoice` model, the AR-relevant fields are:

- `total_amount`
- `paid_amount`
- `balance_amount`
- `status`
- `paid_at`
- `contact_id`
- `invoice_number` / `reference`

Invoices tell you **what remains owed**. Treat `balance_amount` and `status` as authoritative.


### Transactions = ledger events


From the `AccountingTransaction` model:

- `total_amount`
    - **positive = DEBIT**
    - **negative = CREDIT**
- `account_id`
- `type` (examples include `ReceivePayment`, `SalesReceipt`, `CreditMemo`, `CreditRefund`)
- `contacts[]` (with `is_customer` / `is_supplier`)
- `reference`
- `created_at` / `updated_at`

Transactions tell you **what posted**—including customer payments.

> Important: The docs do not guarantee a direct invoice↔transaction foreign key. Reconciliation is therefore best-effort matching, with invoice balances as the final truth.

## Step 3: List invoices (what was owed)


```typescript
const invoices = await sdk.accounting.listAccountingInvoices({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-22T23:22:07.715Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  contact_id: '',
  org_id: '',
  type: '',
  fields: '',
  raw: '',
});
```


### Pagination rules

- Default page size: **100**
- Max page size: **100** on most endpoints
- Pagination uses **offset**
- Stop when `results.length < limit`

Use `updated_gte` to incrementally sync invoice state.


## Step 4: List transactions (what actually happened)


```typescript
const transactions = await sdk.accounting.listAccountingTransactions({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-22T22:41:23.849Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  contact_id: '',
  fields: '',
  raw: '',
});
```


## Step 5: Identify customer payment transactions


Customer payments surface as **transactions** that:

1. Reference a **customer**

    (`contacts[]` contains an entry with `is_customer === true`)

2. Have a **credit** sign

    (`total_amount < 0`)

3. Use a **payment-like type** (examples, not exhaustive):
    - `ReceivePayment`
    - `SalesReceipt`
    - `CreditMemo`
    - `CreditRefund`

Because providers vary, treat the `type` list as **examples**, not a closed set.


Example filter:


```typescript
const customerPayments = transactions.filter(tx => {
  const isCustomer = tx.contacts?.some(c => c.is_customer);
  const isCredit = (tx.total_amount ?? 0) < 0;
  return isCustomer && isCredit;
});
```


## Step 6: Scope reconciliation by customer


Invoices and transactions both reference customers.


```typescript
function byContactId<T extends { contact_id?: string }>(
  rows: T[],
  contactId: string
) {
  return rows.filter(r => r.contact_id === contactId);
}
```


Reconcile **per customer** to avoid cross-account contamination.


## Step 7: Match payments to invoices (best-effort)


Because there is no guaranteed foreign key, matching uses **documented fields only**:


### Safe matching signals

- `contact_id` (must match)
- `reference` / `invoice_number` (when present)
- Amount proximity
- Temporal proximity (`created_at` vs `paid_at`)

### Matching strategy

1. Group invoices by customer.
2. For each invoice:
    - If `balance_amount === 0`, treat as settled.
    - Otherwise, look for customer payment transactions whose:
        - credit magnitude aligns with invoice amount, and/or
        - reference matches invoice reference/number, and/or
        - timestamp falls after invoice `posted_at` and before `paid_at`.

**Always** trust the invoice's `balance_amount` and `status` over heuristic matches.


## Step 8: Compute AR metrics


### Open AR


Sum of `balance_amount` for all non-settled invoices.


```typescript
const openAR = invoices
  .filter(inv => (inv.balance_amount ?? 0) > 0)
  .reduce((sum, inv) => sum + (inv.balance_amount ?? 0), 0);
```


### Paid AR


Sum of `paid_amount` across invoices (or derived from credits if needed).


### Partial payments


Invoices with `paid_amount > 0` and `balance_amount > 0`.


### Credits / overpayments


Invoices or credit transactions where:

- `paid_amount > total_amount`, or
- credit transactions exist without a matching open invoice.

## Step 9: Validate against Accounts Receivable balances (optional)


AR accounts are identified by:


```typescript
type === 'ACCOUNTS_RECEIVABLE'
```


List accounts:


```typescript
const accounts = await sdk.accounting.listAccountingAccounts({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-22T22:41:23.849Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  org_id: '',
  fields: '',
  raw: '',
});
```


Compare:

- **Computed open AR** vs
- **AR account balance**

Discrepancies can occur due to timing or provider posting rules; document them rather than forcing alignment.


## Step 10: Validate with reports (optional, non-deprecated fields only)


### Profit & Loss (validation only)


Retrieve by ID:


```typescript
const pl = await sdk.accounting.getAccountingProfitloss({
  connectionId,
  id: profitlossId,
  fields: '',
  raw: '',
});
```


Use only:

- `income_total_amount`
- `expenses_total_amount`
- `net_income_amount`
- `expenses_sections[].accounts[].transaction_ids`

Do **not** use deprecated fields.


### Balance Sheet (AR snapshot)


Retrieve by ID:


```typescript
const bs = await sdk.accounting.getAccountingBalancesheet({
  connectionId,
  id: balancesheetId,
  fields: '',
  raw: '',
});
```


Use:

- `assets[]` filtered to AR accounts
- `net_assets_amount` (context)

Reports **confirm** reconciliation; they do not replace transaction-based logic.


## Pagination, rate limits, and operations

- All list endpoints use `limit` / `offset`
- Default and max page size is **100**
- Rate limits come from providers and Unified
- Rate-limit errors return **HTTP 429**

**Recommended handling**:

- Exponential backoff with jitter
- Cap retries
- Prefer webhooks for continuous sync

## Summary


Using Unified's Accounting API, you can reconcile customer payments by:

- Treating **invoices** as the AR state
- Treating **transactions** as the ledger of payments
- Matching best-effort with documented fields
- Trusting invoice `balance_amount` and `status` as final
- Validating with AR accounts and non-deprecated reports

The guiding rule is simple:

> Invoices tell you what's owed. Transactions tell you what happened. Reconciliation connects the two—defensively.

→ [Start your 30-day free trial](https://app.unified.to/login)


→ [Book a demo](https://calendly.com/d/cph9-g8n-jzg/connect-with-unified)