Skip to content
Desktop Accounting API

Idempotency and safe retries

Networks drop. When a create times out, you cannot tell whether QuickBooks recorded the invoice. Retrying blindly creates duplicates; giving up loses data. This API gives every write an explicit outcome and lets you retry with an idempotency key, so that a retry is never applied twice.

Send an Idempotency-Key header on any POST or DELETE:

Terminal window
curl https://api.desktopaccountingapi.com/v1/quickbooks-desktop/invoices \
-H "Authorization: Bearer $DAAPI_SECRET_KEY" \
-H "Daapi-End-User-Id: eu_01j9..." \
-H "Idempotency-Key: 3f2c8a10-5b7e-4d21-9c64-8e1f0a2b7d55" \
-H "Content-Type: application/json" \
-d '{"customerId":"80000012-1730312001","lines":[{"itemId":"80000003-1730311000","quantity":2,"rate":"125.00"}]}'
  • Use a new random value, such as a UUID, for each logical operation, and reuse the same value for every retry of that operation.
  • 1 to 255 printable ASCII characters. Anything else returns 400 IDEMPOTENCY_KEY_INVALID.
  • Keys are scoped to the project and end user (or to the project for platform calls such as creating an end user) and kept for 7 days.
  • Reusing a key with a different request (method, path, end user or body) returns 422 IDEMPOTENCY_KEY_REUSED. A key always means one request.

The SDKs generate a key for every write and reuse it across their automatic retries. Pass your own key when the retry happens outside the SDK, for example in a job queue that may run the same job twice:

import { DesktopAccountingApi } from "@desktopaccountingapi/quickbooks-desktop";
const qb = new DesktopAccountingApi().forEndUser("eu_01j9...");
// Derive the key from your own record so a re-run of the job reuses it.
const invoice = await qb.qbd.invoices.create(
{ customerId: "80000012-1730312001", refNumber: "1042" },
{ idempotencyKey: "order-5541-invoice" },
);
console.log(invoice.id);
State of the first request The repeat
Rejected by validation (a 4xx before queueing) Not stored. The repeat is processed as new.
Queued, or sent and waiting for QuickBooks Attaches to the same request and waits for its result (sync), or returns 202 with the same request (async). It never adds a second queue entry.
Finished: succeeded, or failed after QuickBooks answered The stored status and body, with Daapi-Idempotent-Replayed: true.
Ended before it was sent (canceled, expired, timed out in the queue, connection offline) The key is released and the repeat runs as a new request, linked by previousRequestId. This is safe because nothing reached QuickBooks.
outcome_unknown The same QBD_WRITE_OUTCOME_UNKNOWN error. The write is never sent again.

A repeat that arrives while the original is still queued, with a shorter Daapi-Timeout-Seconds, never cancels the original. It gets its own 504 QBD_REQUEST_TIMEOUT and the original keeps running.

Every write response and every write error states an outcome:

outcome Meaning Safe to retry?
applied QuickBooks confirmed the change. No need.
not_applied The change certainly did not happen: validation failed, it was never sent, or QuickBooks returned an error. Yes, after fixing the cause if retryable is false.
pending Sent to QuickBooks, result not known yet. No. Read the request with GET /v1/requests/{id}?waitSeconds=60.
unknown Sent, and the result could not be confirmed. No. See below.
not_applicable A read. Reads are always safe to retry.

Once a write reaches QuickBooks, we never send it again on our own.

A write becomes outcome_unknown when it reached QuickBooks but no answer came back: the customer’s computer lost its network mid-request, the Web Connector was closed, the session broke, or the response could not be read. The API returns 502 QBD_WRITE_OUTCOME_UNKNOWN (type OUTCOME_UNKNOWN_ERROR), the request resource shows status: "outcome_unknown", and the SDKs raise OutcomeUnknownError without retrying.

While a write is uncertain, the connection holds later writes back (waitingReason: "write_recovery_pending") so QuickBooks’ record of that write stays intact. Reads keep flowing.

Every write carries a message set ID derived from its request ID, recorded on the request (quickbooks.messageSetId). The hold on later writes ends when that Web Connector session ends. The uncertain write itself stays outcome_unknown: we never guess, and we never send it again.

Check the company file yourself before writing again:

  1. Look it up by externalId. On creates that support it, we set externalId to a UUID derived from the request when you do not send one, and return it in the error’s details. Query the list (for example GET /v1/quickbooks-desktop/invoices?updatedAfter=...) and match on externalId, or set your own externalId on every create so you always know what to look for.
  2. Or look it up by refNumber or name. Invoices, bills and checks usually carry your reference number; list objects have unique names.
  3. If you find it, use it. If you do not, create it again with a new idempotency key.
  • Let the SDK retry. It only retries what is safe and reuses the key.
  • For raw HTTP, send an Idempotency-Key on every POST and DELETE, and reuse it on retry.
  • Retry only when retryable is true (Daapi-Should-Retry: true), with backoff, honoring Retry-After.
  • On pending, read the request. On unknown, verify. Never resend either.
  • Keep keys for longer than your longest retry window, but no longer than 7 days; after that the API treats the key as new.