Building Accounting Integrations for Expense Management Platforms

This guide provides a complete blueprint for expense management platforms, corporate card providers, neobanks, and vertical SaaS companies looking to integrate with accounting systems through Apideck. It covers the full journey — from initial connection to payment reconciliation — with a focus on user experience and production readiness.

Why Accounting Integrations Matter

Over half of SMEs choose their expense management solution based on the quality of its accounting integrations. For your users, the integration isn't a nice-to-have — it's how they close their books.

A great accounting integration:

  • Eliminates manual data entry — Expenses flow automatically into the accounting system
  • Reduces errors — Proper account mapping means transactions land in the right place
  • Saves time — Accountants spend less time reconciling and reclassifying
  • Increases retention — Users who connect their accounting system are significantly less likely to churn

Integration Architecture Overview

The integration has three layers:

  1. Your application — Expense tracking, mapping settings, and export engine
  2. Apideck Unified API — Vault (auth), Accounting (CRUD), Webhooks (sync)
  3. Downstream providers — QuickBooks, Xero, Exact Online, NetSuite, Sage, Business Central

Your app talks only to Apideck. Apideck handles provider-specific differences, auth token management, and data mapping.


Phase 1: Connection Setup

Embed Apideck Vault

Vault handles the OAuth flow with each accounting provider. Embed it in your application's settings or onboarding flow:

// Create a Vault session for the user
const session = await apideck.vault.sessionsCreate({
  consumerId: userId,
  session: {
    consumer_metadata: {
      account_name: companyName,
      email: userEmail
    }
  }
})

// Redirect user to Vault or embed the component
// session.data.session_uri

Monitor Connection State

Use webhooks to track when connections become active or need re-authorization:

// Key events to handle
'vault.connection.callable'  // Connection ready — start syncing
'vault.connection.invalid'   // Token expired — prompt re-auth
'vault.connection.revoked'   // User disconnected — stop syncing

See the Connection States guide for the full state machine.


Phase 2: Account Mapping

This is the most critical UX step. Your users need to map their expense categories to the correct ledger accounts in their accounting system.

What to Map

Your SideAccounting SideAPI
Expense categoriesLedger accounts (type: expense)GET /accounting/ledger-accounts
Payment methods / cardsBank accounts (type: bank)GET /accounting/ledger-accounts
Merchants / counterpartiesSuppliers / VendorsGET /accounting/suppliers
Tax categoriesTax ratesGET /accounting/tax-rates
Departments / cost centersTracking categoriesGET /accounting/tracking-categories

Building the Mapping UI

Present a two-column interface where users match their categories to accounting accounts:

Your CategoryAccounting Account
Travel6200 - Travel & Entertainment
Office Supplies6100 - Office Expenses
Software6350 - IT Costs
Meals6200 - Travel & Entertainment
Default bank account1100 - Business Account
Default tax rate21% VAT

For detailed implementation guidance, see the Ledger Account Mapping guide.


Phase 3: Exporting Expenses

Choose the Right Resource

ScenarioResourceWhy
Expense already paid (card transaction)ExpenseRecords the payment and categorization in one step
Expense awaiting reimbursement/approvalBillCreates an AP entry that can be paid later
Exact Online (any scenario)BillExpenses not supported — use Bills with due_date = bill_date
Need full debit/credit controlJournal EntryFor complex multi-account transactions

See When to Use Bills vs. Expenses for the detailed decision matrix.

Create an Expense (for QuickBooks, Xero, NetSuite)

const expense = await apideck.accounting.expensesAdd({
  serviceId: connectedService,
  expense: {
    transaction_date: '2025-03-15T12:00:00Z',
    account_id: mapping.bankAccountId,        // Payment account
    supplier_id: mapping.suppliers[merchantId], // Mapped merchant
    currency: 'EUR',
    memo: 'Business lunch - Client meeting',
    line_items: [
      {
        account_id: mapping.categories[categoryId], // Mapped expense account
        description: 'Business lunch',
        total_amount: 45.50,
        tax_rate_id: mapping.defaultTaxRate,
        tracking_categories: [
          { id: mapping.departments[deptId] }
        ]
      }
    ],
    total_amount: 45.50
  }
})

Create a Bill (for Exact Online and reimbursements)

const bill = await apideck.accounting.billsAdd({
  serviceId: 'exact-online',
  bill: {
    bill_number: `EXP-${expenseId}`,
    supplier_id: mapping.suppliers[merchantId],
    bill_date: transactionDate,
    due_date: transactionDate, // Same date = already paid
    currency: 'EUR',
    line_items: [
      {
        account_id: mapping.categories[categoryId],
        description: expenseDescription,
        total_amount: amount,
        tax_rate_id: mapping.defaultTaxRate
      }
    ],
    total: amount,
    status: 'draft'
  }
})

Phase 4: Payment Reconciliation

After creating bills, you need to record the payment to mark them as paid. This is the reconciliation step.

Create a Bill Payment

const billPayment = await apideck.accounting.billPaymentsAdd({
  serviceId: 'exact-online',
  billPayment: {
    supplier_id: bill.supplier_id,
    total_amount: bill.total,
    transaction_date: paymentDate,
    account: {
      id: mapping.bankAccountId // Bank account the payment came from
    },
    allocations: [
      {
        id: bill.id,           // The bill being paid
        type: 'bill',
        amount: bill.total     // Full or partial amount
      }
    ],
    status: 'authorised',
    type: 'accounts_payable',
    currency: 'EUR'
  }
})

The accounting system automatically updates the bill status to paid once the full amount is allocated.

For more details, see the Mark Invoices as Paid guide.


Phase 5: Attachments & Receipts

Attach receipt images to expenses or bills for audit compliance:

const formData = new FormData()
formData.append('file', receiptFile)

await fetch(
  `https://unify.apideck.com/accounting/attachments/bill/${billId}`,
  {
    method: 'POST',
    headers: {
      'x-apideck-consumer-id': consumerId,
      'x-apideck-app-id': appId,
      'x-apideck-service-id': serviceId,
      Authorization: `Bearer ${apiKey}`
    },
    body: formData
  }
)

Phase 6: Error Handling & Monitoring

Handle Common Errors

try {
  await apideck.accounting.billsAdd(billData)
} catch (error) {
  switch (error.status) {
    case 401:
      // Connection expired — redirect to Vault re-auth
      await redirectToVault(consumerId)
      break
    case 422:
      // Validation error — check required fields
      // Common: missing account_id, invalid date, closed period
      logValidationError(error.detail)
      break
    case 429:
      // Rate limited — implement backoff
      await backoff(error.headers['retry-after'])
      break
  }
}

Monitor with Webhooks

app.post('/webhooks/apideck', (req, res) => {
  const { event_type, payload } = req.body

  switch (event_type) {
    case 'accounting.bill.created':
      markExpenseAsSynced(payload.id)
      break
    case 'vault.connection.invalid':
      notifyUserToReconnect(payload.consumer_id)
      break
  }

  res.status(200).send()
})

Development Strategy

Start Simple, Expand Later

  1. Week 1-2: Implement Vault connection + basic bill creation against QuickBooks sandbox
  2. Week 3: Add ledger account mapping UI
  3. Week 4: Add bill payments for reconciliation
  4. Week 5: Test against your target provider (e.g., Exact Online)
  5. Week 6: Add attachments, webhooks, and error handling

Start with QuickBooks or Xero

Even if your primary market uses Exact Online, start development with QuickBooks or Xero:

  • Free sandbox accounts with sample data
  • Best developer documentation
  • Since Apideck provides a unified API, 95% of your code is the same across providers
  • Each connector has setup docs — e.g., Exact Online setup

Test the Remaining 5% Per Provider

Each provider has small differences:

  • Exact Online: Only Bills (no Expenses), XML-based bill payments
  • QuickBooks: Expenses map to Purchases, Classes for tracking
  • Xero: Expenses map to Bank Transactions, Tracking Categories for dimensions
  • NetSuite: Supports Subsidiaries and multi-dimensional tracking

Complete Integration Checklist

AreaRequirement
ConnectionEmbed Vault for OAuth connection flow
Handle connection state webhooks (callable, invalid, revoked)
Support multiple accounting connections per user
MappingFetch and display ledger accounts for mapping
Map expense categories → expense ledger accounts
Map payment methods → bank/card ledger accounts
Map merchants → suppliers (with option to create new)
Map tax categories → tax rates
Optional: Map departments → tracking categories
Implement auto-match by label similarity
Persist mappings per user per connection
ExportCreate Bills for AP / reimbursement expenses
Create Expenses for already-paid transactions (where supported)
Handle line items with correct account, tax, and tracking references
Support multi-currency with exchange rates
Upload receipt attachments
ReconciliationCreate Bill Payments to mark bills as paid
Handle partial payments and overpayments via allocations
Verify bill status updates after payment creation
ProductionError handling for auth failures, validation errors, rate limits
Webhook monitoring for connection health
Retry logic with exponential backoff
Logging for debugging sync failures
User-facing sync status UI

AI Agent Prompt

Use this prompt with your AI coding assistant (Claude, Cursor, Copilot, etc.) to scaffold the integration.

Build an accounting integration for an expense management platform using the
Apideck unified Accounting API. The integration should:

1. VAULT CONNECTION
   - Embed Apideck Vault for OAuth connection flow
   - Use x-apideck-consumer-id, x-apideck-app-id, and Bearer token auth
   - Handle vault.connection.callable, vault.connection.invalid, and
     vault.connection.revoked webhook events
   - Base URL: https://unify.apideck.com

2. LEDGER ACCOUNT MAPPING
   - Fetch ledger accounts: GET /accounting/ledger-accounts (filter by type)
   - Fetch suppliers: GET /accounting/suppliers
   - Fetch tax rates: GET /accounting/tax-rates
   - Build a settings UI where users map their expense categories to
     ledger accounts, payment methods to bank accounts, and merchants
     to suppliers
   - Store mappings per consumer_id + service_id

3. EXPENSE EXPORT
   - For already-paid expenses: POST /accounting/expenses
     (QuickBooks, Xero, NetSuite only — NOT Exact Online)
   - For AP / reimbursements: POST /accounting/bills
     (works on all providers including Exact Online)
   - Each line_item must reference: account_id (mapped ledger account),
     tax_rate_id, and optionally tracking_categories
   - Support multi-currency via currency and exchange_rate fields

4. PAYMENT RECONCILIATION
   - After creating a bill, reconcile with: POST /accounting/bill-payments
   - Link payment to bill via allocations[].id = bill.id
   - Bill status automatically updates to "paid"

5. ATTACHMENTS
   - Upload receipts: POST /accounting/attachments/{reference_type}/{id}
   - Use multipart/form-data, not SDK (file uploads are HTTP-only)

6. ERROR HANDLING
   - 401: redirect to Vault re-auth
   - 422: log validation error details
   - 429: implement exponential backoff using retry-after header

Provider differences:
- Exact Online: Only Bills (no Expenses), bill payments use XML API
  (transparent via Apideck)
- QuickBooks: Expenses map to Purchases, Classes for tracking
- Xero: Expenses map to Bank Transactions, Tracking Categories
- NetSuite: Supports Subsidiaries and multi-dimensional tracking

Reference: https://developers.apideck.com/guides/expense-management-integration
API docs: https://developers.apideck.com/apis/accounting/reference