Skip to content
Desktop Accounting API

Migrating from Conductor

Desktop Accounting API uses the same REST paths, query parameters and JSON field names as Conductor wherever the behavior is the same. A Conductor integration moves over by changing configuration, not by rewriting data mapping. This page lists every difference we know about.

  • Paths. GET /v1/quickbooks-desktop/invoices, POST /v1/quickbooks-desktop/customers/{id}, POST /v1/end-users, POST /v1/auth-sessions, GET /v1/quickbooks-desktop/health-check and every other Conductor operation exist under the same method and path, listed in the API reference. The one exception is a void that QuickBooks itself cannot perform; it returns a clear error with alternatives (see below).
  • Field names, types and enum values. Request bodies, response objects, query filters, enum values and reference objects ({ "id", "fullName" }) use Conductor’s names and JSON types. We compare every parameter, body field and response field of all 262 Conductor operations with our contract, and every difference is one of those listed under Behavior differences. The fields we do not offer belong to the Canadian, UK and Australian editions of QuickBooks, which US company files do not have.
  • Money, prices and percentages as decimal strings, dates as YYYY-MM-DD, revisionNumber on updates, -1 for new lines.
  • List fields. data, nextCursor, hasMore and remainingCount have the same names and types. Continue a list by sending the nextCursor of the latest page as cursor, with or without the original filters; Conductor’s SDKs do exactly that. One difference: every page returns a new cursor, where Conductor documents one cursor that stays the same for the whole list. Code that stores the first cursor and keeps resending it gets the second page again, and then 400 CURSOR_INVALID; switch it to the latest nextCursor (see Behavior differences). Lists that QuickBooks returns in one piece (payroll wage items, templates) answer with nextCursor: null and hasMore: false, so a Conductor pagination loop stops after the first page.
  • Reports. Rows carry kind, rowNumber, text, rowDescriptor and cells, as in Conductor.
  • Error envelope. error.type, code, message, userFacingMessage, httpStatusCode, integrationCode and requestId are present with the same meaning. Common codes such as INTEGRATION_CONNECTION_NOT_SET_UP, INTEGRATION_CONNECTION_NOT_ACTIVE, QBD_CONNECTION_ERROR, QBD_REQUEST_ERROR and API_KEY_INVALID keep their names.
  • Setup links. POST /v1/auth-sessions takes publishableKey, endUserId, linkExpiryMins (15 minutes to 7 days, default 30) and redirectUrl, and returns authFlowUrl.
Conductor Desktop Accounting API
Base URL https://api.conductor.is/v1 https://api.desktopaccountingapi.com/v1
Secret key sk_conductor_... sk_test_... or sk_live_...
Publishable key pk_conductor_... pk_test_... or pk_live_...
End-user header Conductor-End-User-Id Daapi-End-User-Id (Conductor-End-User-Id also works)
Timeout header Conductor-Timeout-Seconds Daapi-Timeout-Seconds, 1 to 300, default 90 (Conductor-Timeout-Seconds also works)
Request ID header Conductor-Request-Id Daapi-Request-Id (Conductor-Request-Id carries the same value)
End-user IDs end_usr_... eu_...
Environment variable read by the SDKs CONDUCTOR_SECRET_KEY DAAPI_SECRET_KEY

Conductor-End-User-Id and Conductor-Timeout-Seconds are accepted as aliases of our header names, so code that sets Conductor’s headers keeps working. If a request carries both names with different values, it fails with 400 INVALID_PARAMETER rather than guessing. Our API rejects unknown Daapi-* headers with 400 UNKNOWN_HEADER, so a typo in a header name fails loudly instead of falling back to a default.

Code written for Conductor’s SDKs runs on ours with two edits: the import and the API key. Our TypeScript and Python SDKs accept Conductor’s end-user parameter (conductorEndUserId, conductor_end_user_id), its client option names and its error class names.

// Before (conductor-node):
// import Conductor from "conductor-node";
// const conductor = new Conductor({ apiKey: process.env.CONDUCTOR_SECRET_KEY });
import Conductor from "@desktopaccountingapi/quickbooks-desktop";
const conductor = new Conductor({ apiKey: process.env.DAAPI_SECRET_KEY });
// Unchanged Conductor code:
const page = await conductor.qbd.invoices.list({ conductorEndUserId: "eu_01j9...", limit: 50 });
console.log(page.data.length);
// Or use a scoped client: conductor.forEndUser("eu_01j9...").qbd.invoices.list({ limit: 50 })

The resource tree is the same (client.qbd.invoices.list, client.endUsers.create, client.qbd.reports.generalSummary). Error classes keep Conductor’s names (APIError, NotFoundError, RateLimitError, …), and Conductor’s err.error.error unwrapping keeps working in TypeScript. Each SDK page’s “Porting from Conductor” section lists what still changes by hand, such as page.getNextPage() loops. Conductor publishes Node.js and Python SDKs; we also publish C# / .NET and Java SDKs with the same resource tree, and their pages map Conductor’s REST settings to them.

If you call the REST API directly, the change is the base URL and the key. Renaming the end-user header to Daapi-End-User-Id is optional.

Conductor’s Node.js SDK works against this API when you set baseURL to https://api.desktopaccountingapi.com/v1 and apiKey to your secret key. We run conductor-node 14.23.8 against our API in our own tests: filtered for await pagination and getNextPage(), create, retrieve and update with conductorEndUserId, and its error object (err.status, err.error.error.code) all work. Errors carry X-Should-Retry, the header that SDK obeys before its own status rules, so it retries rate limits and other safe failures but never resends a write whose outcome is pending or unknown. That SDK sends no Idempotency-Key, so a write that fails on the network can still run twice; move to our SDKs, which send one on every write, or pass the header yourself through the SDK’s per-request headers option.

Conductor’s Web Connector installations talk to Conductor’s servers, so they cannot be moved. Each customer runs our setup flow once:

  1. Create an end user for each customer with POST /v1/end-users. Reuse your own customer ID as sourceId.
  2. Create a setup link with POST /v1/auth-sessions and send it, or show it in your product. See Connect an end user.
  3. The customer downloads our connector file, allows access in QuickBooks and enters the new password. It takes about five minutes.
  4. Ask the customer to remove the old Conductor application from the Web Connector afterwards. Two applications on one Web Connector take turns, which slows both.

QuickBooks record IDs (id, revisionNumber) come from the company file itself, so every ID you stored while using Conductor stays valid. Nothing in your database needs remapping.

These are deliberate. Each one makes a failure visible instead of silent.

Area Conductor Desktop Accounting API
Request status Internal logs only Every QuickBooks call is a request resource: GET /v1/requests/{id} and the dashboard request log show its status, timeline and native status code. See Request lifecycle
Async work and webhooks Not offered Prefer: respond-async returns 202 immediately; signed webhooks report completion. See Webhooks
Error detail message, userFacingMessage, code The same, plus cause, fixes with who should act, retryable, outcome, param, details and docsUrl. See Error codes
Idempotency Server-side behavior not documented Idempotency-Key on every write; a repeated key never writes twice; uncertain writes return QBD_WRITE_OUTCOME_UNKNOWN instead of being retried. See Idempotency
Cursor expiry Iteration can fail mid-stream after about 10 seconds idle Same QuickBooks limit, made explicit: cursorExpiresAt on every page, replay of the latest page after a network error, 410 CURSOR_EXPIRED with progress counts, and SDK read-ahead for slow loops (a loop that stops early runs no extra query). See Pagination
Cursor values One cursor for the whole list A new nextCursor on every page. Resending a cursor repeats its page (a safe retry), at most twice; then 400 CURSOR_INVALID. Resending the original filters with the cursor is accepted; changing them returns 400 CURSOR_PARAMS_MISMATCH
ids, fullNames, refNumbers with other filters Other filters ignored limit is ignored (the lookup is one page). Any other filter returns 400 INVALID_PARAMETER naming both, because QuickBooks cannot apply it and dropping it silently could return records you filtered out
GET /v1/end-users Every end user in one response Paged: limit 1 to 100, default 50, with nextCursor and hasMore
Automatic retries SDK retries 408, 409, 429 and 5xx Our SDKs retry only what Daapi-Should-Retry allows; for Conductor’s SDK, X-Should-Retry gives the same answer. Writes with a pending or unknown outcome are never resent
Read-only MCP x-stainless-mcp-client-permissions header or --code-allow-http-gets Both are honored and turn on read-only mode; ?read_only=true, --read-only and read-only secret keys work too. See MCP
Billing blocks Reported in message only Typed errors BILLING_REQUIRED and PAYMENT_FAILED (402) for you; your end user still sees a neutral message
salesTaxPaymentChecks.void Listed Returns 422 QBD_OPERATION_UNSUPPORTED with alternatives, because QuickBooks has no void request for that transaction type. See Supported environments
payrollWageItems.list, templates.list Cursor paging Returns all records in one page (hasMore: false); QuickBooks offers no cursor for these lists. A cursor parameter is rejected with 400 CURSOR_INVALID
Report rows A union of four row types keyed by kind One row object with the same fields; fields that do not apply to a row kind are null or []
Response fields QuickBooks omits Declared non-null in most places null. Damaged company files leave out elements QuickBooks normally sends; the response still arrives, with a warning on the request (see Damaged and unusual data)
priceLevels.list?itemIds Array One item ID; QuickBooks filters price levels by a single item
Fields of non-US editions (salesTaxCountry, purchase tax codes, line tax codes on purchases and similar) Listed Not offered: we connect US editions of QuickBooks Desktop, which do not have these fields
Inputs QuickBooks requires (for example creditCardTransaction.request and .response, inventoryAdjustments lines) Optional in the schema Required; QuickBooks rejects the request without them, so we reject it first and name the parameter
Unknown body fields Not documented Rejected with 400 UNKNOWN_PARAMETER, so a misspelled field never gets dropped silently
More decimal places than QuickBooks stores Not documented Rejected with 400 DECIMAL_PRECISION_EXCEEDED, never rounded
  • Create a test project, a sk_test_ key and a test end user. Run your integration tests against a sample company file.
  • Replace the base URL, key and header names in configuration.
  • Handle the extra error fields if you want them (retryable, outcome, fixes); existing code that reads code and userFacingMessage keeps working.
  • Add Idempotency-Key to raw HTTP writes. The SDKs already send one.
  • Create a production project and sk_live_ key.
  • Create end users and setup links for each customer, and track who has completed setup with the health check or the connection.setup_completed webhook.
  • Remove the old application from each customer’s Web Connector.