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 install conductor-node
yarn add conductor-node
npm install conductor-node
yarn add conductor-node
npm install conductor-node
yarn add conductor-node
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:
create a Conductor end-user
store the returned ID in your database
create an auth session
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