---
title: "How to Reconcile Vendor Payments (AP) with Unified's Accounting API"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_reconcile_vendor_payments_ap_with_unified_accounting_api-icon.png
date: 2026-01-19T21:56:00.000Z
tag: Product
description: "Accounts Payable reconciliation is often implemented backwards."
url: "https://unified.to/blog/how_to_reconcile_vendor_payments_ap_with_unified_accounting_api"
---

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

Accounts Payable reconciliation is often implemented backwards.


Teams start by looking for payment events—checks, ACHs, credit card charges—and try to infer AP state from them. That can work in a single accounting system, but it breaks as soon as you support multiple providers. Payment activity shows _movement_. AP is about _obligation_.


For PMs and backend teams, this creates immediate risk:

- Is AP driven by what was paid, or what the ledger still says is owed?
- How do partial payments, vendor credits, and overpayments actually surface?
- Which number should users trust when bills and transactions don't line up perfectly?

When AP logic is built on payment-like transactions alone, edge cases accumulate fast. Bills appear unpaid even after payments. Credits drift. Vendor balances become hard to explain.


Unified's [Accounting API](/accounting) is designed to anchor AP reconciliation where it belongs: in the general ledger. In Unified Accounting, **bills define what you owe**, and **transactions define what posted**. Vendor payments are not a separate first-class object—they are ledger entries that must be interpreted in context.


This guide shows how to reconcile AP defensively using that model—combining bills, transactions, contacts, and accounts, matching payments best-effort using documented fields only, and treating bill balances as the final authority—so your AP logic stays correct across accounting systems.


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


Unified has a separate **Payments** API category for payment processor events (Stripe/Square/etc.). That is not what AP is.


AP reconciliation is a ledger problem: 'what does the accounting system say we owe and have paid?'


So for AP, you use:

- `AccountingBill` for vendor obligations
- `AccountingTransaction` for payment events and ledger movement

## What this guide covers

- Listing bills and transactions with [pagination](/docs/reference/pagination) and incremental sync
- Identifying vendor payment transactions
- Matching vendor payments to bills (best-effort, doc-accurate)
- Computing open AP, paid AP, partials, and vendor credits
- Validating against AP accounts (optional)

## Prerequisites

- Node.js 18+
- A Unified workspace
- An Accounting integration authorized for a customer
- A valid `connectionId`
- [Unified API](/technology) 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!,
  },
});
```


## Step 2: Understand the two source objects


### Bills represent 'what you owe'


Bills (`AccountingBill`) provide the AP state you care about:

- `total_amount`
- `paid_amount`
- `balance_amount`
- `status`
- `due_at`, `paid_at`
- `contact_id` (vendor)
- `bill_number`

Bills are the authoritative 'owed vs paid' state.


### Transactions represent 'what happened'


Transactions (`AccountingTransaction`) are ledger records with key semantics:

- `total_amount`
    - **positive = DEBIT**
    - **negative = CREDIT**
- `type` includes examples like:
    - `BillPaymentCheck`
    - `BillPaymentCreditCard`
    - `VendorCredit`
    - `Credit`
    - `Transfer`, `Deposit` (noise for AP)
- `contacts[]` with `is_supplier`
- `account_id`
- `payment_method`
> Important: Unified docs do not guarantee a direct bill↔transaction foreign key. So reconciliation is best-effort matching, with bill balance_amount as the final authority.

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


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


Pagination rules:

- Default `limit`: 100
- Uses `offset`
- Stop when returned results `< limit`

## Step 4: Retrieve a single bill (optional)


Useful for verifying results or refreshing state after matching.


```typescript
const bill = await sdk.accounting.getAccountingBill({
  connectionId,
  id: billId,
  fields: '',
  raw: '',
});
```


## Step 5: List transactions (what actually hit the ledger)


(Uses the same list endpoint you used in your runway post.)


```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 6: Identify vendor payment transactions


Vendor payments surface as transactions that:

1. reference a vendor

    (`contacts[]` contains an entry where `is_supplier === true`)

2. represent an outflow / AP reduction

    (typically **credit** entries: `total_amount < 0`, but provider accounting can vary)

3. have a payment-like `type` (examples, non-exhaustive):
- `BillPaymentCheck`
- `BillPaymentCreditCard`
- `VendorCredit`
- `Credit`

Filter example:


```typescript
const vendorPayments = transactions.filter(tx => {
  const isSupplier = tx.contacts?.some(c => c.is_supplier);
  if (!isSupplier) return false;

  const type = tx.type || '';
  const looksLikeAPPayment =
    type.includes('BillPayment') ||
    type === 'VendorCredit' ||
    type === 'Credit';

  return looksLikeAPPayment;
});
```


This avoids hard-coding a closed list of types while still using documented examples.


## Step 7: Scope reconciliation by vendor


Bills and transactions both relate to a vendor via `contact_id` (bills) and `contacts[]` (transactions).


Bills:

- `bill.contact_id`

Transactions:

- `tx.contacts[].id` (vendor id)
- plus `is_supplier`

Reconcile per vendor to avoid cross-contamination.


## Step 8: Match payments to bills (best-effort)


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


### Safe matching signals

- Vendor identity (`contact_id` vs transaction contact `id`)
- Amount proximity (credit magnitude vs bill balance)
- Time proximity (`created_at` / `updated_at` vs `paid_at`)
- `reference` fields where present

### Matching strategy


For each open bill (`balance_amount > 0`):

- find vendor payment transactions with:
    - same vendor
    - payment amount close to balance or total
    - timestamp near bill `paid_at` or within a payment window

**Final truth remains bill** **`balance_amount`** **and** **`status`****.** If those don't change, you should treat the bill as unpaid even if you see a payment-like transaction.


## Step 9: Compute AP metrics


### Open AP


Sum of bill balances that are still owed:


```typescript
const openAP = bills
  .filter(b => (b.balance_amount ?? 0) > 0)
  .reduce((sum, b) => sum + (b.balance_amount ?? 0), 0);
```


### Paid AP


Bills where:

- `paid_amount > 0` and/or
- `status` indicates paid or partially paid

### Partial payments


Bills where:

- `paid_amount > 0`
- `balance_amount > 0`

### Vendor credits


Handled as:

- transactions like `VendorCredit`
- bills with refund amounts
- or bills whose paid amount exceeds expected

## Step 10: Validate with AP accounts (optional)


If you want a secondary sanity check, validate open AP against accounts typed as:

- `ACCOUNTS_PAYABLE`

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 AP

    vs

- AP account balance

Discrepancies can occur due to provider posting rules and timing. Don't force equality.


## Updating bills (optional)


Updating bills exists, but should be used carefully because providers may restrict updates based on bill lifecycle.


```typescript
const updated = await sdk.accounting.updateAccountingBill({
  connectionId,
  id: billId,
  accountingBill: { notes: 'Updated notes' },
  fields: '',
  raw: '',
});
```


## Rate limits, pagination, and reliability

- All list endpoints use `limit` and `offset`
- Default max is 100 records per request
- Use `updated_gte` for incremental sync
- Rate limits return **HTTP 429**
- Use exponential backoff + jitter for retries
- For continuous sync, prefer webhooks rather than polling

## Summary


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

- treating **bills** as your canonical AP state (`balance_amount`, `paid_amount`, `status`)
- treating **transactions** as ledger payment activity (`type`, `total_amount`, vendor contact)
- matching defensively (because no guaranteed bill↔transaction join)
- validating against AP accounts (optional)

The guiding rule:

> Bills tell you what you owe. Transactions tell you what happened. Reconciliation connects them—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)