How to Build Accounting Integrations with Claude Code

Learn how to build accounting integrations with Claude Code using the QuickBooks MCP server, Apideck MCP server, and Apideck unified API. Includes working code examples for OAuth, invoice creation, and multi-platform support.

Saurabh RaiSaurabh Rai

Saurabh Rai

11 min read
How to Build Accounting Integrations with Claude Code

Most accounting integrations start the same way. A developer opens the QuickBooks API docs, reads through the OAuth 2.0 flow, finds out there are seventeen different account types, realizes that Xero uses a completely different data model, and spends the next two weeks writing plumbing that will need to be rewritten every time a new accounting platform gets added to the roadmap.

Claude Code changes how this work gets done. Not because it writes boilerplate faster (though it does), but because the combination of agentic coding, MCP tool access, and unified APIs removes entire categories of the problem. This post walks through four ways to build accounting integrations: calling platform APIs directly, connecting via the QuickBooks MCP server, connecting via the Apideck MCP server, and using Apideck's unified accounting API for production programmatic integrations.

Direct API: QuickBooks as a starting point

The direct approach is the right choice when you are building against a single accounting platform and want full control over the data model. Claude Code handles the OAuth dance, the pagination logic, and the error handling without you having to think through each step.

Here is what a working Claude Code session looks like when you want to create an invoice in QuickBooks Online:

claude "Write a Node.js function that authenticates with QuickBooks Online using OAuth 2.0 and creates an invoice for a customer. Use the intuit-oauth and node-quickbooks packages. The invoice should accept customer ID, line items with amount and description, and a due date. Handle token refresh automatically."

Claude Code will produce something close to this:

const OAuthClient = require('intuit-oauth');
const QuickBooks = require('node-quickbooks');

const oauthClient = new OAuthClient({
  clientId: process.env.QBO_CLIENT_ID,
  clientSecret: process.env.QBO_CLIENT_SECRET,
  environment: 'production',
  redirectUri: process.env.QBO_REDIRECT_URI,
});

async function createInvoice({ customerId, lineItems, dueDate, realmId, token }) {
  if (oauthClient.isAccessTokenValid() === false) {
    const authResponse = await oauthClient.refreshUsingToken(token.refresh_token);
    token = authResponse.getJson();
  }

  const qbo = new QuickBooks(
    process.env.QBO_CLIENT_ID,
    process.env.QBO_CLIENT_SECRET,
    token.access_token,
    false,
    realmId,
    true,
    false,
    null,
    '2.0',
    token.refresh_token
  );

  const invoice = {
    CustomerRef: { value: customerId },
    DueDate: dueDate,
    Line: lineItems.map((item, index) => ({
      Id: String(index + 1),
      LineNum: index + 1,
      Amount: item.amount,
      DetailType: 'SalesItemLineDetail',
      SalesItemLineDetail: {
        ItemRef: { value: '1', name: 'Services' },
        UnitPrice: item.amount,
        Qty: 1,
        TaxCodeRef: { value: 'NON' },
      },
      Description: item.description,
    })),
  };

  return new Promise((resolve, reject) => {
    qbo.createInvoice(invoice, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

This works. But now you need the same functionality for Xero. Xero's invoice creation endpoint uses Invoices (plural), the line item structure is different, and the authentication token exchange follows a different PKCE flow. Claude Code can write that version too, but now you have two codepaths to maintain and two different webhook parsers for the same business event.

That maintenance cost compounds. If you add Sage, FreshBooks, or NetSuite, you are not just adding connectors. You are adding three separate mental models for what an "invoice" or a "bill" means, and the divergence grows with every platform.

Via QuickBooks' own MCP server

If your integration only needs to cover QuickBooks Online, Intuit's own MCP server is the most direct path. They published intuit/quickbooks-online-mcp-server on GitHub, which covers 143 tools across 29 entity types with full CRUD operations, plus 11 financial reports including Balance Sheet, P&L, and Cash Flow.

Clone and build it first:

git clone https://github.com/intuit/quickbooks-online-mcp-server.git
cd quickbooks-online-mcp-server
npm install && npm run build

Then register it in Claude Code via .claude.json:

{
  "mcpServers": {
    "quickbooks": {
      "command": "node",
      "args": ["path/to/quickbooks-online-mcp-server/dist/index.js"],
      "env": {
        "QUICKBOOKS_CLIENT_ID": "your_client_id",
        "QUICKBOOKS_CLIENT_SECRET": "your_client_secret",
        "QUICKBOOKS_REFRESH_TOKEN": "your_refresh_token",
        "QUICKBOOKS_REALM_ID": "your_realm_id",
        "QUICKBOOKS_ENVIRONMENT": "sandbox"
      }
    }
  }
}

A note on auth. The QUICKBOOKS_REFRESH_TOKEN value is not something the MCP server generates for you. You get it by completing the OAuth 2.0 authorization flow once, either through Intuit's developer portal or your own callback endpoint. QBO access tokens expire after one hour; the server handles rotation automatically from there. What you are responsible for is storing the latest refresh token after each rotation, since QBO invalidates the previous one on every exchange. For personal dev tooling, the .env approach above is fine. For a product where each of your customers connects their own QBO account, you need a backend that captures per-user refresh tokens, stores them, and injects the right one at session time rather than using a shared env var.

Once registered, Claude Code has live access to your QBO company data:

claude "Using the QuickBooks MCP, pull all open invoices, identify customers with more than $10,000 outstanding, and generate a collections priority report in markdown."

Or inside a development session:

claude "Read the chart of accounts from the QuickBooks MCP and scaffold a TypeScript module that maps each account to our internal ledger category enum. Write tests for the mapping logic."

The tradeoff is that this server runs as a local stdio process, tied to a single QBO company per session. For a product where users connect different accounting platforms, you need a different approach.

Via Apideck MCP server: multi-platform agentic access

The QuickBooks MCP server covers one platform per process. If your agent needs to work across whatever accounting system a given user has connected — QuickBooks for one customer, Xero for another, Sage for a third — you need a different MCP layer.

Apideck's Unified MCP server runs over HTTP transport and routes each tool call to the correct platform based on the consumer ID you pass in the headers. Register it in Claude Code:

claude mcp add apideck --transport http https://mcp.apideck.dev/mcp \
  --header "x-apideck-api-key: YOUR_API_KEY" \
  --header "x-apideck-app-id: YOUR_APP_ID" \
  --header "x-apideck-consumer-id: YOUR_CONSUMER_ID"

Or configure it directly in .claude.json:

{
  "mcpServers": {
    "apideck": {
      "url": "https://mcp.apideck.dev/mcp",
      "headers": {
        "x-apideck-api-key": "YOUR_API_KEY",
        "x-apideck-app-id": "YOUR_APP_ID",
        "x-apideck-consumer-id": "YOUR_CONSUMER_ID"
      }
    }
  }
}

Connecting users via Vault

The x-apideck-consumer-id header only works if the user has already authorized their accounting platform. That authorization happens through Vault, Apideck's hosted connection UI. You generate a Vault session link server-side and redirect the user to it. They pick their accounting platform, complete the OAuth flow, and Vault handles storing the tokens. Your code never touches their credentials.

Generating a session link takes one API call:

import { Apideck } from '@apideck/unify'

async function createVaultSession(consumerId) {
  const apideck = new Apideck({
    apiKey: process.env.APIDECK_API_KEY,
    appId: process.env.APIDECK_APP_ID,
    consumerId,
  })

  const { data } = await apideck.vault.sessions.create({
    session: {
      unified_apis: ['accounting'],
      redirect_uri: 'https://yourapp.com/dashboard',
      theme: {
        vault_name: 'Your App',
        primary_color: '#your-brand-color',
      },
    },
  })

  return data.session_uri  // redirect the user to this URL
}

The response includes a session_uri which you forward to the user. Once they complete the flow, that consumer ID is live and ready to use in the MCP headers. If you prefer to embed the connection experience inside your app rather than redirecting, @apideck/vault-js opens it as a modal with a single method call.

From there, the same Claude Code prompts work regardless of which platform the user has connected:

claude "Using the Apideck MCP, pull the last 60 days of transactions for this customer, categorize any uncategorized expenses against our chart of accounts, and flag any amounts over $5,000 for review."

This is also the right setup for testing across multiple platforms before committing to a production REST integration. Switch the consumer ID, run the same prompt against a QBO sandbox and a Xero sandbox, and verify the normalized response looks consistent before writing any application code.

Via Apideck unified API: production deterministic integrations

The cleanest mental model for choosing between MCP and REST: use MCP when the agent decides what to call and when; use the REST API when your own application code is making those decisions. An AI bookkeeper that reads invoices and flags anomalies belongs on MCP. A scheduled sync job that runs every hour belongs on the REST API.

The unified accounting API is the default for companies adding accounting integrations as a product feature. You write the integration once against Apideck's normalized schema, and your users can connect QuickBooks, Xero, Sage, NetSuite, FreshBooks, or any of the 26 supported platforms without you touching the integration layer.

Creating an invoice across any connected accounting platform looks like this:

const Apideck = require('@apideck/node');

const apideck = new Apideck({
  apiKey: process.env.APIDECK_API_KEY,
  appId: process.env.APIDECK_APP_ID,
  consumerId: req.user.consumerId,
});

async function createInvoice({ lineItems, customerId, dueDate }) {
  const response = await apideck.accounting.invoicesAdd({
    invoice: {
      type: 'accounts_receivable',
      customer: { id: customerId },
      due_date: dueDate,
      line_items: lineItems.map(item => ({
        description: item.description,
        unit_price: item.amount,
        quantity: 1,
        total_amount: item.amount,
      })),
      currency: 'USD',
    },
  });

  return response.data;
}

The same function works whether the end user has connected QuickBooks, Xero, or Sage Business Cloud. Apideck normalizes the data model on the way in and maps the response back to the same schema on the way out.

Claude Code accelerates this further. Instead of reading through the Apideck API reference and writing the integration from scratch, you can give Claude Code a task:

claude "Using the Apideck Node SDK, build a sync module that pulls chart of accounts from the connected accounting platform, maps each account to our internal category taxonomy stored in accounts-taxonomy.json, and writes the mapping to Postgres. Handle pagination. Log any accounts that don't match a category."

Claude Code reads accounts-taxonomy.json, inspects the SDK types, writes the sync logic, and generates the Postgres schema. You can combine the Apideck MCP and the REST API in the same session: use the MCP server to pull live sample data during development, then use Claude Code to build the production REST integration against that data.

claude "Using the Apideck MCP, pull a sample of 20 invoices from the test QBO connection. Then write a TypeScript function that replicates that invoice creation via the Apideck REST API. Test against the sandbox environment."

Which approach fits which problem

ApproachBest forLimitation
Direct APISingle-platform builds, platform-specific fields, white-labeled products with a fixed accounting systemOne codebase per platform; maintenance cost grows with every new connector added
QuickBooks MCP serverAgentic workflows scoped to QBO: AI agents that read and write QBO data on behalf of users, plus dev-time exploration and testingTied to a single QBO company per process; no multi-platform support
Apideck MCP serverAgentic workflows that need to reach across multiple accounting platforms; AI agents where the platform is determined at runtime by the connected accountNot the right layer for deterministic programmatic integrations where your application controls the logic
Apideck unified APIProgrammatic integrations where your application drives the logic: syncing, webhooks, scheduled jobs, multi-tenant products shipping accounting features to end users at scaleNormalized schema means some platform-specific fields are not exposed

Claude Code makes all four paths faster. For most product teams, the practical setup is: Apideck MCP for exploration and agent-driven workflows, Apideck REST API for production programmatic integrations, and Claude Code doing the implementation across both.

Ready to get started?

Scale your integration strategy and deliver the integrations your customers need in record time.

Ready to get started?
Talk to an expert

Trusted by fast-moving product & engineering teams

JobNimbus
Blue Zinc
Exact
Drata
Octa
Apideck Blog

Insights, guides, and updates from Apideck

Discover company news, API insights, and expert blog posts. Explore practical integration guides and tech articles to make the most of Apideck's platform.