QuickBooks Desktop Node.js API for JavaScript and TypeScript

Learn how to use QuickBooks Desktop or QuickBooks Enterprise from Node.js with Conductor, including setup, health checks, invoices, and reports.

If you want a QuickBooks Desktop Node.js API, what you usually want is not raw access to qbXML or QuickBooks Web Connector. You want to work with QuickBooks Desktop from TypeScript or JavaScript using normal API patterns.

That is exactly where Conductor fits.

Quick answers

  • Can I use Node.js with QuickBooks Desktop? Yes.

  • Can I use TypeScript too? Yes.

  • Does this work for QuickBooks Enterprise? Yes.

  • Is there a native QuickBooks Desktop REST API for Node.js? Not natively. Conductor gives you a modern API and SDK layer on top of the Desktop stack.

Developers searching for QuickBooks Desktop Node.js, QuickBooks Desktop JavaScript API, or QuickBooks Desktop TypeScript API are usually trying to avoid the native Desktop stack:

  • qbXML

  • Web Connector SOAP callbacks

  • COM or Windows-only tooling

  • vague Desktop-side errors

That instinct is right. If your app is already in Node.js or TypeScript, you should keep your product logic in that ecosystem instead of pulling legacy QuickBooks mechanics into your application code.

Get started in Node.js

Use the Conductor Node SDK:

# npm
npm install conductor-node

# yarn
yarn add conductor-node

# pnpm

# npm
npm install conductor-node

# yarn
yarn add conductor-node

# pnpm

# npm
npm install conductor-node

# yarn
yarn add conductor-node

# pnpm

Initialize the client

import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

That is the base client you will use for QuickBooks Desktop requests.

Example: create an end-user and auth session

If you are building a multi-tenant product, the usual first step is:

  1. create a Conductor end-user

  2. store the returned ID in your database

  3. create an auth session

  4. send the user through the QuickBooks Desktop connection flow

import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

async function createQuickBooksDesktopConnection() {
  const endUser = await conductor.endUsers.create({
    companyName: "Northwind Mechanical LLC",
    sourceId: "acct_48291",
    email: "ap@northwind.example",
  });

  const authSession = await conductor.authSessions.create({
    publishableKey: process.env.CONDUCTOR_PUBLISHABLE_KEY!,
    endUserId: endUser.id,
    linkExpiryMins: 60 * 24,
  });

  return {
    conductorEndUserId: endUser.id,
    authFlowUrl: authSession.authFlowUrl,
  };
}

createQuickBooksDesktopConnection().then(console.log).catch(console.error);
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

async function createQuickBooksDesktopConnection() {
  const endUser = await conductor.endUsers.create({
    companyName: "Northwind Mechanical LLC",
    sourceId: "acct_48291",
    email: "ap@northwind.example",
  });

  const authSession = await conductor.authSessions.create({
    publishableKey: process.env.CONDUCTOR_PUBLISHABLE_KEY!,
    endUserId: endUser.id,
    linkExpiryMins: 60 * 24,
  });

  return {
    conductorEndUserId: endUser.id,
    authFlowUrl: authSession.authFlowUrl,
  };
}

createQuickBooksDesktopConnection().then(console.log).catch(console.error);
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

async function createQuickBooksDesktopConnection() {
  const endUser = await conductor.endUsers.create({
    companyName: "Northwind Mechanical LLC",
    sourceId: "acct_48291",
    email: "ap@northwind.example",
  });

  const authSession = await conductor.authSessions.create({
    publishableKey: process.env.CONDUCTOR_PUBLISHABLE_KEY!,
    endUserId: endUser.id,
    linkExpiryMins: 60 * 24,
  });

  return {
    conductorEndUserId: endUser.id,
    authFlowUrl: authSession.authFlowUrl,
  };
}

createQuickBooksDesktopConnection().then(console.log).catch(console.error);

This is much cleaner than building your own QuickBooks Desktop setup flow around Web Connector and .qwc distribution.

Example: check the connection status

Before you fetch or write data, check whether the QuickBooks Desktop connection is actually ready.

import Conductor, { APIError as ConductorError } from "conductor-node";

interface IConductorError {
  message: string;
  userFacingMessage: string;
  type: string;
  code: string;
  httpStatusCode: number;
  integrationCode?: string;
  requestId: string;
}

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

async function ensureQuickBooksReady(conductorEndUserId: string) {
  try {
    await conductor.qbd.healthCheck({ conductorEndUserId });
    return { ready: true as const };
  } catch (error) {
    if (error instanceof ConductorError) {
      const conductorError = error?.error?.error as IConductorError;
      return {
        ready: false as const,
        code: conductorError.code,
        message: conductorError.userFacingMessage,
      };
    }

    throw error;
  }
}
import Conductor, { APIError as ConductorError } from "conductor-node";

interface IConductorError {
  message: string;
  userFacingMessage: string;
  type: string;
  code: string;
  httpStatusCode: number;
  integrationCode?: string;
  requestId: string;
}

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

async function ensureQuickBooksReady(conductorEndUserId: string) {
  try {
    await conductor.qbd.healthCheck({ conductorEndUserId });
    return { ready: true as const };
  } catch (error) {
    if (error instanceof ConductorError) {
      const conductorError = error?.error?.error as IConductorError;
      return {
        ready: false as const,
        code: conductorError.code,
        message: conductorError.userFacingMessage,
      };
    }

    throw error;
  }
}
import Conductor, { APIError as ConductorError } from "conductor-node";

interface IConductorError {
  message: string;
  userFacingMessage: string;
  type: string;
  code: string;
  httpStatusCode: number;
  integrationCode?: string;
  requestId: string;
}

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

async function ensureQuickBooksReady(conductorEndUserId: string) {
  try {
    await conductor.qbd.healthCheck({ conductorEndUserId });
    return { ready: true as const };
  } catch (error) {
    if (error instanceof ConductorError) {
      const conductorError = error?.error?.error as IConductorError;
      return {
        ready: false as const,
        code: conductorError.code,
        message: conductorError.userFacingMessage,
      };
    }

    throw error;
  }
}

This is one of the most important differences from native QuickBooks Desktop integration. You get a clean health-check flow and a user-facing error model instead of raw Desktop behavior.

Example: fetch invoices from QuickBooks Desktop

import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

const conductorEndUserId = process.env.CONDUCTOR_END_USER_ID!;

async function listRecentInvoices() {
  const invoices = await conductor.qbd.invoices.list({
    conductorEndUserId,
    updatedAfter: "2026-01-01T00:00:00Z",
    limit: 25,
  });

  return invoices.data.map((invoice) => ({
    id: invoice.id,
    refNumber: invoice.refNumber,
    totalAmount: invoice.totalAmount,
    updatedAt: invoice.updatedAt,
  }));
}

listRecentInvoices().then(console.log).catch(console.error);
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

const conductorEndUserId = process.env.CONDUCTOR_END_USER_ID!;

async function listRecentInvoices() {
  const invoices = await conductor.qbd.invoices.list({
    conductorEndUserId,
    updatedAfter: "2026-01-01T00:00:00Z",
    limit: 25,
  });

  return invoices.data.map((invoice) => ({
    id: invoice.id,
    refNumber: invoice.refNumber,
    totalAmount: invoice.totalAmount,
    updatedAt: invoice.updatedAt,
  }));
}

listRecentInvoices().then(console.log).catch(console.error);
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

const conductorEndUserId = process.env.CONDUCTOR_END_USER_ID!;

async function listRecentInvoices() {
  const invoices = await conductor.qbd.invoices.list({
    conductorEndUserId,
    updatedAfter: "2026-01-01T00:00:00Z",
    limit: 25,
  });

  return invoices.data.map((invoice) => ({
    id: invoice.id,
    refNumber: invoice.refNumber,
    totalAmount: invoice.totalAmount,
    updatedAt: invoice.updatedAt,
  }));
}

listRecentInvoices().then(console.log).catch(console.error);

This is the kind of code developers usually mean when they search for a Node.js or TypeScript API.

Example: fetch native reports through passthrough

If you need QuickBooks Desktop reports, you can still access them through Conductor from Node.js.

import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

const conductorEndUserId = process.env.CONDUCTOR_END_USER_ID!;

async function fetchTrialBalance() {
  const report = await conductor.endUsers.passthrough(
    conductorEndUserId,
    "quickbooks_desktop",
    {
      GeneralSummaryReportQueryRq: {
        GeneralSummaryReportType: "TrialBalance",
      },
    },
  );

  console.log(JSON.stringify(report, null, 2));
}

fetchTrialBalance().catch(console.error);
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

const conductorEndUserId = process.env.CONDUCTOR_END_USER_ID!;

async function fetchTrialBalance() {
  const report = await conductor.endUsers.passthrough(
    conductorEndUserId,
    "quickbooks_desktop",
    {
      GeneralSummaryReportQueryRq: {
        GeneralSummaryReportType: "TrialBalance",
      },
    },
  );

  console.log(JSON.stringify(report, null, 2));
}

fetchTrialBalance().catch(console.error);
import Conductor from "conductor-node";

const conductor = new Conductor({
  apiKey: process.env.CONDUCTOR_SECRET_KEY!,
});

const conductorEndUserId = process.env.CONDUCTOR_END_USER_ID!;

async function fetchTrialBalance() {
  const report = await conductor.endUsers.passthrough(
    conductorEndUserId,
    "quickbooks_desktop",
    {
      GeneralSummaryReportQueryRq: {
        GeneralSummaryReportType: "TrialBalance",
      },
    },
  );

  console.log(JSON.stringify(report, null, 2));
}

fetchTrialBalance().catch(console.error);

That is useful when the integration needs accounting-specific report output rather than only operational objects.

Why TypeScript is a particularly good fit

TypeScript works especially well here because:

  • the request shapes are easier to understand

  • the SDK is much easier to use than raw qbXML

  • editor autocomplete helps developers move faster

  • coding agents working in TypeScript codebases can integrate more safely

That is one reason Conductor is a much better fit than the native Desktop stack for modern JavaScript teams.

Common mistakes

Avoid these mistakes:

  • trying to build directly on raw Web Connector callbacks from a Node app

  • treating QuickBooks Desktop like an always-on cloud API

  • skipping connection health checks

  • exposing raw technical error text directly to end-users instead of using user-facing error messages

Frequently asked questions

Does this work for QuickBooks Enterprise too?

Yes. QuickBooks Enterprise is still part of the broader QuickBooks Desktop stack, and the same Node.js integration approach applies.

Should I use TypeScript or JavaScript?

Either works. TypeScript is usually the better developer experience for larger apps and agent-assisted coding workflows.

Is this a native QuickBooks Desktop REST API?

Not natively. Conductor provides the modern API and SDK surface that JavaScript and TypeScript developers usually want.

Bottom line

If you want a QuickBooks Desktop Node.js API, the right approach is to use a modern SDK and API layer instead of dragging qbXML, SOAP, and Web Connector logic into your app.

Conductor gives Node.js, JavaScript, and TypeScript developers a practical way to work with QuickBooks Desktop and QuickBooks Enterprise using normal application code.

Related reading