# Error handling
Source: https://www.desktopaccountingapi.com/docs/guides/error-handling/

> The error object, the nine error types, when to retry, what to show your customer, and typed SDK errors in TypeScript, Python, C# and Java.

Integrations with QuickBooks Desktop fail in ordinary ways: the customer's computer is off, QuickBooks has a dialog open, a name already exists. Every error from this API tells you which kind of failure it is, whether retrying can help, whether a write reached QuickBooks, and what to tell your customer. The full list of codes is in the [error code reference](https://www.desktopaccountingapi.com/docs/errors/).

## The error object

Every error response has this body, with the HTTP status in `httpStatusCode` as well:

```json
{
  "error": {
    "type": "INTEGRATION_ERROR",
    "code": "QBD_DUPLICATE_NAME",
    "message": "QuickBooks returned status 3100: The name \"Northwind Traders\" of the list element is already in use.",
    "userFacingMessage": "A record with this name already exists in QuickBooks Desktop.",
    "httpStatusCode": 409,
    "integrationCode": "3100",
    "requestId": "req_01j9x4m6v4c8k2t7q0r5s3w1zb",
    "cause": "QuickBooks names must be unique within a list, and across customers, vendors, employees and other names.",
    "fixes": [{ "actor": "developer", "action": "Use a different name, or look up and reuse the existing object." }],
    "docsUrl": "https://www.desktopaccountingapi.com/docs/errors/#qbd_duplicate_name",
    "retryable": false,
    "outcome": "not_applied",
    "param": "name",
    "details": { "requestId": "req_01j9x4m6v4c8k2t7q0r5s3w1zb", "qbxmlStatusCode": 3100, "qbxmlStatusMessage": "The name \"Northwind Traders\" of the list element is already in use." }
  }
}
```

| Field | Meaning |
| --- | --- |
| `type` | One of nine categories below. Route on it. |
| `code` | A stable code from the [catalog](https://www.desktopaccountingapi.com/docs/errors/). A code's meaning never changes. |
| `message` | For you: specific, may contain IDs and field paths, never secrets. Log it. |
| `userFacingMessage` | For your customer: plain language, always present. Authentication, billing and internal problems are masked to a neutral sentence. |
| `httpStatusCode` | The HTTP status of the response. |
| `integrationCode` | The native code when QuickBooks or the Web Connector produced one: a qbXML status code (`"3100"`), a COM HRESULT (`"0x80040414"`) or a Web Connector code (`"QBWC1039"`). |
| `requestId` | The request ID (`req_...`), also in the `Daapi-Request-Id` header. |
| `cause`, `fixes` | Why it happened, and what to do. Each fix names who acts: `developer`, `end_user` or `support`. |
| `docsUrl` | The section of the error reference for this code. |
| `retryable` | Whether repeating the same request, unchanged, can succeed. Also sent as the `Daapi-Should-Retry` header. The [error reference](https://www.desktopaccountingapi.com/docs/errors/#retry-guidance) gives each code a fuller retry rule. |
| `outcome` | Whether a write changed the company file: `applied`, `not_applied`, `pending`, `unknown`, or `not_applicable` for reads. |
| `param` | The request field at fault, as a path such as `lines[2].amount`, when known. |
| `details` | Extra data for some codes, such as `pagesServed` on `CURSOR_EXPIRED`, `minimumQbxmlVersion` on version errors, or a ranked `diagnosis` on timeouts and `QBD_QUICKBOOKS_NOT_RESPONDING`. The error reference lists the keys per code. |

## Error types

| Type | What happened | How to handle it |
| --- | --- | --- |
| `INVALID_REQUEST_ERROR` | Your request is malformed or breaks a rule. Nothing reached QuickBooks. | Fix the code. Do not retry unchanged. |
| `AUTHENTICATION_ERROR` | Missing, invalid or revoked key. | Check configuration and alert yourself. |
| `PERMISSION_ERROR` | Valid key, action not allowed (wrong project, disabled connection). | Check the project and connection. |
| `BILLING_ERROR` | Production data requests are blocked by billing. | Fix billing in the dashboard. See [Billing](https://www.desktopaccountingapi.com/docs/platform/billing/). |
| `RATE_LIMIT_ERROR` | Too many requests. | Wait for `Retry-After` and retry. See [Rate limits](https://www.desktopaccountingapi.com/docs/guides/rate-limits/). |
| `INTEGRATION_CONNECTION_ERROR` | Something on the customer's computer: offline, QuickBooks closed or busy, a dialog open, the wrong file. | Show `userFacingMessage`, retry later if `retryable`. Log as a warning, not an incident. |
| `INTEGRATION_ERROR` | QuickBooks received the request and rejected it, for example a duplicate name or stale revision. | Usually a data problem to fix in code. `integrationCode` holds the qbXML status. |
| `OUTCOME_UNKNOWN_ERROR` | A write was sent and its result could not be confirmed. | Never resend it. See [uncertain writes](https://www.desktopaccountingapi.com/docs/guides/idempotency/#uncertain-writes). |
| `INTERNAL_ERROR` | A problem on our side. | Retry reads. For writes, check `outcome` first. |

New types and codes can appear. Handle unknown values as a generic failure and keep `userFacingMessage` as the message.

## Deciding whether to retry

`retryable` (and the `Daapi-Should-Retry` header) answers one question: can the identical request succeed later without anyone changing it? Use it together with `outcome`:

| `retryable` | `outcome` | Do this |
| --- | --- | --- |
| `true` | `not_applied` | Retry with backoff, honoring `Retry-After`. Reuse the same `Idempotency-Key` for writes. |
| `false` | `not_applied` | Do not retry. Fix the request, or ask the customer to fix their setup. |
| any | `pending` | Do not resend. The request is still running; read it with `GET /v1/requests/{id}?waitSeconds=60`. |
| any | `unknown` | Do not resend. Verify in QuickBooks first. |
| any | `applied` | The change happened. Do not resend. |

The SDKs follow this table for you: they retry network failures, `429` and `5xx` responses whose `Daapi-Should-Retry` is `true`, with exponential backoff from 0.5 to 8 seconds, up to two retries by default, and never when `outcome` is `pending` or `unknown`.

## Typed errors in the SDKs

Each SDK raises one error class per `type`, with every field available as a property.

**TypeScript**

```ts
import {
  DesktopAccountingApi,
  ApiError,
  IntegrationConnectionError,
  OutcomeUnknownError,
} from "@desktopaccountingapi/quickbooks-desktop";

const qb = new DesktopAccountingApi().forEndUser("eu_01j9...");

try {
  await qb.qbd.customers.create({ name: "Northwind Traders" });
} catch (err) {
  if (err instanceof IntegrationConnectionError) {
    console.warn(err.code, err.requestId);
    // show err.userFacingMessage to your customer
  } else if (err instanceof OutcomeUnknownError) {
    // do not retry: look the customer up by name or externalId first
  } else if (err instanceof ApiError && err.code === "QBD_DUPLICATE_NAME") {
    // reuse the existing customer
  } else {
    throw err;
  }
}
```

**Python**

```python
from desktopaccountingapi import (
    APIError,
    DesktopAccountingApi,
    IntegrationConnectionError,
    OutcomeUnknownError,
)

qb = DesktopAccountingApi().for_end_user("eu_01j9...")

try:
    qb.qbd.customers.create(name="Northwind Traders")
except IntegrationConnectionError as err:
    print(err.code, err.request_id)  # show err.user_facing_message to your customer
except OutcomeUnknownError:
    pass  # do not retry: look the customer up by name or external_id first
except APIError as err:
    if err.code != "QBD_DUPLICATE_NAME":
        raise
```

**C#**

```csharp
try
{
    await qb.Qbd.Customers.CreateAsync(new CustomerCreateInput { Name = "Northwind Traders" });
}
catch (IntegrationConnectionException ex)
{
    logger.LogWarning("{Code} {RequestId}", ex.Code, ex.RequestId);
    // show ex.UserFacingMessage to your customer
}
catch (OutcomeUnknownException)
{
    // do not retry: look the customer up by name or ExternalId first
}
catch (ApiException ex) when (ex.Code == "QBD_DUPLICATE_NAME")
{
    // reuse the existing customer
}
```

**Java**

```java
try {
    qb.qbd().customers().create(new CustomerCreateInput("Northwind Traders"));
} catch (IntegrationConnectionException e) {
    log.warn("{} {}", e.getCode(), e.getRequestId());
    // show e.getUserFacingMessage() to your customer
} catch (OutcomeUnknownException e) {
    // do not retry: look the customer up by name or externalId first
} catch (ApiException e) {
    if (!"QBD_DUPLICATE_NAME".equals(e.getCode())) throw e;
}
```

| Type | TypeScript | Python | C# | Java |
| --- | --- | --- | --- | --- |
| Base class | `ApiError` | `APIError` | `ApiException` | `ApiException` |
| `INVALID_REQUEST_ERROR` | `InvalidRequestError` | `InvalidRequestError` | `InvalidRequestException` | `InvalidRequestException` |
| `AUTHENTICATION_ERROR` | `AuthenticationError` | `AuthenticationError` | `AuthenticationException` | `AuthenticationException` |
| `PERMISSION_ERROR` | `PermissionError` | `PermissionDeniedError` | `PermissionException` | `PermissionException` |
| `BILLING_ERROR` | `BillingError` | `BillingError` | `BillingException` | `BillingException` |
| `RATE_LIMIT_ERROR` | `RateLimitError` | `RateLimitError` | `RateLimitException` | `RateLimitException` |
| `INTEGRATION_CONNECTION_ERROR` | `IntegrationConnectionError` | `IntegrationConnectionError` | `IntegrationConnectionException` | `IntegrationConnectionException` |
| `INTEGRATION_ERROR` | `IntegrationError` | `IntegrationError` | `IntegrationException` | `IntegrationException` |
| `OUTCOME_UNKNOWN_ERROR` | `OutcomeUnknownError` | `OutcomeUnknownError` | `OutcomeUnknownException` | `OutcomeUnknownException` |
| `INTERNAL_ERROR` | `InternalError` | `InternalError` | `InternalException` | `InternalException` |
| `CURSOR_EXPIRED` | `CursorExpiredError` | `CursorExpiredError` | `CursorExpiredException` | `CursorExpiredException` |
| No response at all | `ApiConnectionError` | `APIConnectionError` | `ApiConnectionException` | `ApiConnectionException` |

## A pattern for production

- **Branch on `type` first, then on the few `code` values you handle specially.** Treat everything else in a type the same way.
- **Show `userFacingMessage`, not `message`, to customers.** It is safe by design, and for connection errors it tells them exactly what to do on their computer. Link the [help center](https://www.desktopaccountingapi.com/docs/help/) next to it.
- **Alert on your own failures, not your customers'.** `AUTHENTICATION_ERROR`, `INVALID_REQUEST_ERROR`, `INTERNAL_ERROR` and a rise in `INTEGRATION_ERROR` deserve attention. `INTEGRATION_CONNECTION_ERROR` is usually a customer's PC being off.
- **Store `requestId` with failures.** The dashboard request log opens it directly, with the timeline and native QuickBooks status, and support can find it immediately.
- **Prefer user-started syncs.** When a sync runs because a person clicked a button, a connection error becomes a message they can act on. For scheduled syncs, record the last error per customer and show it in your UI.

## QuickBooks status codes

When QuickBooks rejects a request, `integrationCode` holds its qbXML status code and `details.qbxmlStatusMessage` holds QuickBooks' own text. Common ones map to dedicated codes:

| qbXML status | Code |
| --- | --- |
| 3100 | `QBD_DUPLICATE_NAME` |
| 3120, 3140 | `QBD_OBJECT_NOT_FOUND` for the ID in the path, `QBD_REFERENCE_NOT_FOUND` for a referenced ID (with `param`) |
| 3175, 3176 | `QBD_OBJECT_IN_USE` (someone has the record open in QuickBooks; for a deletion, other records may still use it) |
| 3200 | `QBD_REVISION_NUMBER_STALE` |
| 3250 | `QBD_FEATURE_NOT_ENABLED` |
| 3260 | `QBD_INSUFFICIENT_PERMISSION` (the QuickBooks user's role) |
| 3261 | `QBD_INSUFFICIENT_PERMISSION` (the app may not access personal data, for example payroll reports) |
| Any other error status | `QBD_REQUEST_ERROR` |

Connection-level failures carry the COM HRESULT that QuickBooks returned, such as `0x80040414` for an open dialog or `0x8004040A` for the wrong company file. The [error reference](https://www.desktopaccountingapi.com/docs/errors/) lists every mapped code.
