Building a Ledger Account Mapping UI

When integrating with accounting systems, one of the most critical steps is allowing your users to map their internal categories to the correct ledger accounts (chart of accounts) in their downstream accounting system. This guide walks you through building a mapping experience that works across all major accounting providers via Apideck's unified API.


Why Ledger Account Mapping Matters

Every accounting system organizes financial data around a chart of accounts — a structured list of ledger accounts that categorize transactions (e.g., "Office Supplies", "Travel Expenses", "Revenue"). When your application creates bills, expenses, or journal entries, each line item needs to reference the correct ledger account in the user's accounting system.

Without proper mapping:

  • Transactions land in the wrong accounts, breaking financial reports
  • Accountants spend hours manually reclassifying entries
  • Users lose trust in your integration

How It Works

The mapping flow is straightforward:

  1. Your categories (left side) — The expense categories, revenue types, or transaction types defined in your application
  2. Their ledger accounts (right side) — The chart of accounts from the user's connected accounting system, fetched via Apideck
  3. User maps — The user selects which ledger account corresponds to each of your categories
Your CategoryDownstream Account
Travel Expenses6200 - Travel & Entertainment
Office Supplies6100 - Office Expenses
Software6350 - IT Costs
Meals & Entertainment6200 - Travel & Entertainment
Professional Services6400 - Prof. Services

Step 1: Fetch Ledger Accounts

Use the Ledger Accounts API to retrieve the user's chart of accounts from their connected accounting system:

const accounts = await apideck.accounting.ledgerAccountsAll({
  serviceId: 'exact-online' // or 'quickbooks', 'xero', 'netsuite'
})

// Filter client-side by account type
const expenseAccounts = accounts.data.filter(
  (account) => account.type === 'expense'
)

expenseAccounts.forEach((account) => {
  console.log(`${account.nominal_code} - ${account.name} (${account.type})`)
})

Common Account Types

TypeUse Case
type: 'expense'Expense categories, cost of goods
type: 'revenue'Income and sales categories
type: 'bank'Bank and credit card accounts (funding sources)
type: 'asset'Asset accounts
type: 'liability'Liability accounts (accounts payable, credit cards)

Step 2: Build the Mapping UI

Your mapping interface should present the user's categories on one side and a searchable dropdown of ledger accounts on the other.

Your mapping settings page should include:

  • Header: "Map your categories to [Provider Name] accounts"
  • Each row: Your category label on the left, a searchable dropdown of ledger accounts on the right
  • Dropdown: Shows account code + name (e.g., "6200 - Travel & Entertainment"), with search filtering
  • Actions: An "Auto-Match" button for label-based matching and a "Save" button to persist

Key UX Recommendations

  • Searchable dropdowns — Chart of accounts can have hundreds of entries. Let users search by name or account code.
  • Group by type — Group accounts by their type (Expense, Revenue, Asset, etc.) in the dropdown.
  • Show account codes — Display the nominal code alongside the account name (e.g., "6200 - Travel & Entertainment"). Accountants rely on these codes.
  • Auto-match option — Offer a button that attempts to match categories to accounts based on label similarity (see Step 3).
  • Persist mappings — Store the mapping in your database so users only configure it once per connection.

Step 3: Auto-Match by Label

You can reduce manual work by auto-matching your categories to ledger accounts based on name similarity:

function autoMatch(yourCategories, ledgerAccounts) {
  const mappings = {}

  for (const category of yourCategories) {
    const categoryLower = category.name.toLowerCase()

    // Try exact match first
    let match = ledgerAccounts.find(
      (a) => a.name.toLowerCase() === categoryLower
    )

    // Then try partial match
    if (!match) {
      match = ledgerAccounts.find(
        (a) =>
          a.name.toLowerCase().includes(categoryLower) ||
          categoryLower.includes(a.name.toLowerCase())
      )
    }

    if (match) {
      mappings[category.id] = {
        accountId: match.id,
        accountName: match.name,
        confidence: match.name.toLowerCase() === categoryLower
          ? 'exact'
          : 'partial'
      }
    }
  }

  return mappings
}

Step 4: Use Mappings When Creating Transactions

Once mappings are stored, use them when creating bills, expenses, or journal entries:

// Retrieve stored mapping for this connection
const mapping = await getStoredMapping(consumerId, serviceId)

// Create a bill with mapped accounts
const bill = await apideck.accounting.billsAdd({
  serviceId: 'exact-online',
  bill: {
    bill_number: 'EXP-2025-001',
    supplier_id: mapping.defaultSupplierId,
    bill_date: '2025-03-15',
    due_date: '2025-04-15',
    line_items: expenses.map((expense) => ({
      account_id: mapping.categories[expense.categoryId].accountId,
      description: expense.description,
      total_amount: expense.amount
    })),
    total: expenses.reduce((sum, e) => sum + e.amount, 0),
    status: 'draft'
  }
})

Step 5: Map Additional Entities

Beyond ledger accounts, you may also need to map:

Suppliers / Vendors

const suppliers = await apideck.accounting.suppliersAll({
  serviceId: 'exact-online'
})
// Allow users to map merchants to existing suppliers
// or create new ones via suppliersAdd

Tax Rates

const taxRates = await apideck.accounting.taxRatesAll({
  serviceId: 'exact-online'
})
// Map your tax categories to the provider's tax rates

Tracking Categories

For department or project-level tracking, also map tracking categories:

const trackingCategories = await apideck.accounting.trackingCategoriesAll({
  serviceId: 'exact-online'
})

See the Tracking Dimensions guide for details.


Provider-Specific Notes

Exact Online

  • Ledger accounts are called GLAccounts (General Ledger Accounts)
  • Account codes are numeric and specific to Belgian/Dutch accounting standards (MAR/MAR-BE)
  • Cost centers and cost units provide additional categorization
  • Only Bills are supported for expense transactions (not Expenses)

QuickBooks

  • Uses a flat chart of accounts structure
  • Classes provide additional categorization beyond accounts
  • Account types are well-defined (Expense, Cost of Goods Sold, Other Expense)

Xero

  • Account codes are user-defined and may vary significantly between organizations
  • Tracking categories provide the additional dimension for categorization
  • Bank accounts are separate from expense accounts

NetSuite

  • Supports the most complex chart of accounts with subsidiaries
  • Departments, Classes, and Locations provide multi-dimensional tracking
  • Account numbers follow a hierarchical structure

Best Practices

  1. Fetch accounts on connection setup — Retrieve and cache the chart of accounts when a user first connects their accounting system, not at transaction time.
  2. Handle account changes — Periodically refresh the account list. Accounts can be added, renamed, or deactivated.
  3. Provide defaults — If your application has standard categories, suggest common mappings that users can adjust.
  4. Validate before sending — Before creating transactions, verify that all referenced account_id values still exist in the downstream system.
  5. Support unmapped categories — Have a fallback account (e.g., "Miscellaneous Expenses") for categories the user hasn't mapped yet.
  6. Store per connection — Each user's accounting system has different accounts. Store mappings per consumer_id + service_id combination.