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:
AccountingBillfor vendor obligationsAccountingTransactionfor 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_amountpaid_amountbalance_amountstatusdue_at,paid_atcontact_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
typeincludes examples like:BillPaymentCheckBillPaymentCreditCardVendorCreditCreditTransfer,Deposit(noise for AP)
contacts[]withis_supplieraccount_idpayment_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:
- reference a vendor
(contacts[]contains an entry whereis_supplier === true) - represent an outflow / AP reduction
(typically credit entries:total_amount < 0, but provider accounting can vary) - have a payment-like
type(examples, non-exhaustive):
BillPaymentCheckBillPaymentCreditCardVendorCreditCredit
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_idvs transaction contactid) - Amount proximity (credit magnitude vs bill balance)
- Time proximity (
created_at/updated_atvspaid_at) referencefields 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_ator 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);
Paid AP
Bills where:
paid_amount > 0and/orstatusindicates paid or partially paid
Partial payments
Bills where:
paid_amount > 0balance_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
limitandoffset - Default max is 100 records per request
- Use
updated_gtefor 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.