# 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](/guides/vault) handles the OAuth flow with each accounting provider. Embed it in your application's settings or onboarding flow:

```javascript
// 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](/guides/webhooks) to track when connections become active or need re-authorization:

```javascript
// 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](/guides/connection-states) 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 Side | Accounting Side | API |
|-----------|----------------|-----|
| Expense categories | Ledger accounts (type: expense) | [`GET /accounting/ledger-accounts`](/apis/accounting/reference#tag/Ledger-Accounts) |
| Payment methods / cards | Bank accounts (type: bank) | [`GET /accounting/ledger-accounts`](/apis/accounting/reference#tag/Ledger-Accounts) |
| Merchants / counterparties | Suppliers / Vendors | [`GET /accounting/suppliers`](/apis/accounting/reference#tag/Suppliers) |
| Tax categories | Tax rates | [`GET /accounting/tax-rates`](/apis/accounting/reference#tag/Tax-Rates) |
| Departments / cost centers | Tracking categories | [`GET /accounting/tracking-categories`](/apis/accounting/reference#tag/Tracking-Categories) |

### Building the Mapping UI

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

| Your Category | Accounting Account |
|---|---|
| Travel | 6200 - Travel & Entertainment |
| Office Supplies | 6100 - Office Expenses |
| Software | 6350 - IT Costs |
| Meals | 6200 - Travel & Entertainment |
| **Default bank account** | 1100 - Business Account |
| **Default tax rate** | 21% VAT |

For detailed implementation guidance, see the [Ledger Account Mapping guide](/guides/ledger-account-mapping).

---

## Phase 3: Exporting Expenses

### Choose the Right Resource

| Scenario | Resource | Why |
|----------|----------|-----|
| Expense already paid (card transaction) | **Expense** | Records the payment and categorization in one step |
| Expense awaiting reimbursement/approval | **Bill** | Creates an AP entry that can be paid later |
| Exact Online (any scenario) | **Bill** | Expenses not supported — use Bills with `due_date = bill_date` |
| Need full debit/credit control | **Journal Entry** | For complex multi-account transactions |

See [When to Use Bills vs. Expenses](/guides/expenses-bills#when-to-use-bills-vs-expenses) for the detailed decision matrix.

### Create an Expense (for QuickBooks, Xero, NetSuite)

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

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

```javascript
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](/guides/mark-invoices-as-paid).

>
> Bill Payments for Exact Online use the XML API under the hood. This is handled transparently by Apideck — your API calls remain the same REST format.

---

## Phase 5: Attachments & Receipts

Attach receipt images to expenses or bills for audit compliance:

```javascript
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}`,
      'x-apideck-metadata': JSON.stringify({
        name: receiptFile.name,
        description: 'Expense receipt'
      })
    },
    body: formData
  }
)
```

---

## Phase 6: Error Handling & Monitoring

### Handle Common Errors

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

```javascript
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](/connectors/exact-online-nl/docs/application_owner+oauth_credentials)

### 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

| Area | Requirement |
|------|-------------|
| **Connection** | Embed Vault for OAuth connection flow |
| | Handle connection state webhooks (callable, invalid, revoked) |
| | Support multiple accounting connections per user |
| **Mapping** | Fetch 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 |
| **Export** | Create 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 |
| **Reconciliation** | Create Bill Payments to mark bills as paid |
| | Handle partial payments and overpayments via allocations |
| | Verify bill status updates after payment creation |
| **Production** | Error 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.

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

---

## Related Guides

- [Ledger Account Mapping](/guides/ledger-account-mapping) — Building the mapping UI
- [Integrating Expenses and Bills](/guides/expenses-bills) — Detailed Bills vs. Expenses guide
- [Mark Invoices as Paid](/guides/mark-invoices-as-paid) — Payment allocation
- [Accounting Data Model](/guides/accounting-data-model) — Entity relationships
- [Vault](/guides/vault) — Embedding the connection UI
- [Webhooks](/guides/webhooks) — Real-time event monitoring
- [Tracking Dimensions](/guides/locations-subsidiaries-departments) — Departments and locations
