Unified.to
All articles

How to Sync and Classify Expenses Using Unified's Accounting API


January 20, 2026

Expense ingestion looks simple until you try to make it work across accounting systems.

Different providers disagree on where classification lives, which fields are required, and what can be created programmatically. Some enforce account-level classification. Others require categories or tax rates at the line-item level. Many silently recompute totals or reject writes that don't align with their internal rules.

For a PM or backend team, this creates early risk:

  • Can expenses be written safely across providers without conditional logic?
  • Where should classification live so that downstream reporting stays consistent?
  • How do you avoid partial writes, duplicate expenses, or reconciliation drift?

Most teams end up hardcoding provider-specific behavior or narrowing scope to a single accounting system. That works initially, but it doesn't scale.

Unified's Accounting API is designed to make expense sync predictable. Expenses, accounts, categories, and tax rates are shared through normalized models with documented behavior. Classification is explicit, line-item driven, and validated by the provider at write time rather than inferred upstream.

This guide shows how to build a production-grade expense sync flow—fetching, classifying, creating, and updating expenses—using Unified's normalized API and documented parameters only.

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!,
  },
});

All requests are scoped by connectionId, which represents a single authorized customer integration.

Step 2: Understand the Expense model

Unified represents expenses using the normalized AccountingExpense object.

Key points:

  • Classification happens at the line-item level
  • Each line item can reference:
    • account_idAccountingAccount
    • category_ids[]AccountingCategory
    • taxrate_idAccountingTaxrate
  • All fields are optional at the schema level; provider rules are enforced downstream

You should never assume which fields are required for a given provider.

Step 3: List expenses (with pagination and incremental sync)

Use listAccountingExpenses to fetch expenses.

const results = await sdk.accounting.listAccountingExpenses({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-20T00:59:41.301Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  user_id: '',
  start_gte: '',
  end_lt: '',
  contact_id: '',
  category_id: '',
  group_id: '',
  fields: '',
  raw: '',
});

Pagination rules

  • Default page size: 100
  • Maximum page size: 100 on most endpoints
  • Pagination uses offset, not cursors
  • You've reached the end when results.length < limit

Incremental sync

Use updated_gte to fetch only records updated since your last sync timestamp.

This is the recommended approach for polling-based sync.

Step 4: Resolve classification dependencies

Before creating or updating expenses, resolve the IDs required for classification.

List categories

const categories = await sdk.accounting.listAccountingCategories({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-20T00:59:41.301Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  parent_id: '',
  fields: '',
  raw: '',
});

List accounts (chart of accounts)

const accounts = await sdk.accounting.listAccountingAccounts({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-20T00:59:41.301Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  org_id: '',
  fields: '',
  raw: '',
});

List tax rates

const taxrates = await sdk.accounting.listAccountingTaxrates({
  connectionId,
  limit: 50,
  offset: 0,
  updated_gte: '2026-01-20T00:59:41.301Z',
  sort: 'updated_at',
  order: 'asc',
  query: '',
  org_id: '',
  fields: '',
  raw: '',
});

Optional: create missing dependencies

If your product auto-provisions classifications, you can create them.

Create a category

await sdk.accounting.createAccountingCategory({
  connectionId,
  accountingCategory: {
    name: 'Travel',
    is_active: true,
  },
});

Create an account

await sdk.accounting.createAccountingAccount({
  connectionId,
  accountingAccount: {
    name: 'Travel Expenses',
    type: 'EXPENSE',
    status: 'ACTIVE',
    currency: 'USD',
  },
});

Do not assume providers allow creation. Always be prepared to handle 501 Not Implemented.

Step 5: Build a normalized expense payload

A minimal, provider-safe expense payload looks like this:

const expense = {
  name: 'Flight to NYC',
  currency: 'USD',
  total_amount: 650.0,
  lineitems: [
    {
      unit_quantity: 1,
      unit_amount: 600.0,
      tax_amount: 50.0,
      account_id: travelAccountId,
      category_ids: [travelCategoryId],
      taxrate_id: salesTaxId,
      notes: 'Conference travel',
    },
  ],
};

Notes:

  • Totals are typically derived as unit_quantity * unit_amount + tax_amount
  • Providers may recompute totals or reject mismatches
  • Leave fields unset unless you are certain they're supported

Step 6: Create the expense

const created = await sdk.accounting.createAccountingExpense({
  connectionId,
  accountingExpense: expense,
});

The response is a single AccountingExpense object reflecting the provider's state.

Step 7: Update an existing expense

To update an expense, supply its id.

const updated = await sdk.accounting.updateAccountingExpense({
  connectionId,
  id: expenseId,
  accountingExpense: {
    ...expense,
    name: 'Updated flight expense',
  },
});

Updates fully replace the resource state. There is no versioning.

Step 8: Retrieve an expense by ID

const result = await sdk.accounting.getAccountingExpense({
  connectionId,
  id: expenseId,
  fields: '',
  raw: '',
});

Use this to verify writes or reconcile downstream changes.

Final notes

Expense sync is one of the fastest ways to accumulate integration debt if classification rules aren't enforced early.

Unified's Accounting API gives you a stable foundation for expense ingestion and classification across accounting systems, without vendor-specific code paths or undocumented behavior.

Start your 30-day free trial

Book a demo

All articles