# Migrating from Conductor
Source: https://www.desktopaccountingapi.com/docs/get-started/migrating-from-conductor/

> Move a Conductor integration to Desktop Accounting API. Same paths, field names and headers; change the base URL and key, then reconnect each customer.

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

- **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](https://www.desktopaccountingapi.com/docs/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](#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](#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`.

## 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

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.

**TypeScript**

```ts
// 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 })
```

**Python**

```python
# 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](https://www.desktopaccountingapi.com/docs/sdks/dotnet/) and [Java](https://www.desktopaccountingapi.com/docs/sdks/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

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

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](https://www.desktopaccountingapi.com/docs/connect/setup-flow/).
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.

## 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](https://www.desktopaccountingapi.com/docs/guides/request-lifecycle/) |
| Async work and webhooks | Not offered | `Prefer: respond-async` returns `202` immediately; signed webhooks report completion. See [Webhooks](https://www.desktopaccountingapi.com/docs/guides/webhooks/) |
| Error detail | `message`, `userFacingMessage`, `code` | The same, plus `cause`, `fixes` with who should act, `retryable`, `outcome`, `param`, `details` and `docsUrl`. See [Error codes](https://www.desktopaccountingapi.com/docs/errors/) |
| 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](https://www.desktopaccountingapi.com/docs/guides/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](https://www.desktopaccountingapi.com/docs/guides/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](https://www.desktopaccountingapi.com/docs/guides/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](https://www.desktopaccountingapi.com/docs/platform/supported-environments/#documented-incompatibilities) |
| `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](https://www.desktopaccountingapi.com/docs/quickbooks/conventions/#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

- [ ] 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.
