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.
What stays the same
Section titled “What stays the same”- 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-checkand 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,revisionNumberon updates,-1for new lines. - List fields.
data,nextCursor,hasMoreandremainingCounthave the same names and types. Continue a list by sending thenextCursorof the latest page ascursor, 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 then400 CURSOR_INVALID; switch it to the latestnextCursor(see Behavior differences). Lists that QuickBooks returns in one piece (payroll wage items, templates) answer withnextCursor: nullandhasMore: false, so a Conductor pagination loop stops after the first page. - Reports. Rows carry
kind,rowNumber,text,rowDescriptorandcells, as in Conductor. - Error envelope.
error.type,code,message,userFacingMessage,httpStatusCode,integrationCodeandrequestIdare present with the same meaning. Common codes such asINTEGRATION_CONNECTION_NOT_SET_UP,INTEGRATION_CONNECTION_NOT_ACTIVE,QBD_CONNECTION_ERROR,QBD_REQUEST_ERRORandAPI_KEY_INVALIDkeep their names. - Setup links.
POST /v1/auth-sessionstakespublishableKey,endUserId,linkExpiryMins(15 minutes to 7 days, default 30) andredirectUrl, and returnsauthFlowUrl.
What you change
Section titled “What you change”| 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.
SDK calls
Section titled “SDK calls”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 })# Before (conductor-py):# from conductor import Conductor# conductor = Conductor(api_key=os.environ["CONDUCTOR_SECRET_KEY"])import os
from desktopaccountingapi import DesktopAccountingApi as Conductor
conductor = Conductor(api_key=os.environ["DAAPI_SECRET_KEY"])
# Unchanged Conductor code:for invoice in conductor.qbd.invoices.list(conductor_end_user_id="eu_01j9...", limit=50): print(invoice.ref_number)# Or use a scoped client: conductor.for_end_user("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.
Keeping Conductor’s SDK for now
Section titled “Keeping Conductor’s SDK for now”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.
Reconnecting your customers
Section titled “Reconnecting your customers”Conductor’s Web Connector installations talk to Conductor’s servers, so they cannot be moved. Each customer runs our setup flow once:
- Create an end user for each customer with
POST /v1/end-users. Reuse your own customer ID assourceId. - Create a setup link with
POST /v1/auth-sessionsand send it, or show it in your product. See Connect an end user. - The customer downloads our connector file, allows access in QuickBooks and enters the new password. It takes about five minutes.
- 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.
Behavior differences
Section titled “Behavior differences”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 |
Migration checklist
Section titled “Migration checklist”- 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 readscodeanduserFacingMessagekeeps working. - Add
Idempotency-Keyto 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_completedwebhook. - Remove the old application from each customer’s Web Connector.