# 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 Category | | Downstream Account |
|---|---|---|
| Travel Expenses | → | 6200 - Travel & Entertainment |
| Office Supplies | → | 6100 - Office Expenses |
| Software | → | 6350 - IT Costs |
| Meals & Entertainment | → | 6200 - Travel & Entertainment |
| Professional Services | → | 6400 - Prof. Services |

---

## Step 1: Fetch Ledger Accounts

Use the [Ledger Accounts API](/apis/accounting/reference#tag/Ledger-Accounts) to retrieve the user's chart of accounts from their connected accounting system:

```javascript
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

| Type | Use 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.

### Recommended UI Pattern

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:

```javascript
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
}
```

>
> Auto-matching is a convenience feature. Always let users review and override auto-matched accounts before saving.

---

## Step 4: Use Mappings When Creating Transactions

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

```javascript
// 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

```javascript
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

```javascript
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:

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

See the [Tracking Dimensions guide](/guides/locations-subsidiaries-departments) 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.

---

## Related Resources

- [Integrating Expenses and Bills](/guides/expenses-bills) — End-to-end expense integration guide
- [Mark Invoices as Paid](/guides/mark-invoices-as-paid) — Bill payment and reconciliation
- [Tracking Dimensions](/guides/locations-subsidiaries-departments) — Departments, locations, and subsidiaries
- [Field Mapping](/guides/field-mapping) — Extend Apideck models with custom fields
- [Ledger Accounts API Reference](/apis/accounting/reference#tag/Ledger-Accounts)
