Unified.to
All articles

How to Reconcile Vendor Payments (AP) with Unified's Accounting API


January 27, 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 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 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 key

Step 1: Initialize the SDK

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)

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.

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.)

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:

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:

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

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:

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.

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

Book a demo

All articles