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.
Idempotency keys
Section titled “Idempotency keys”Send an Idempotency-Key header on any POST or DELETE:
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);from desktopaccountingapi import DesktopAccountingApi
qb = DesktopAccountingApi().for_end_user("eu_01j9...")
# Derive the key from your own record so a re-run of the job reuses it.invoice = qb.qbd.invoices.create( customer_id="80000012-1730312001", ref_number="1042", idempotency_key="order-5541-invoice",)print(invoice.id)var invoice = await qb.Qbd.Invoices.CreateAsync( new InvoiceCreateInput { CustomerId = "80000012-1730312001", RefNumber = "1042" }, new RequestOptions { IdempotencyKey = "order-5541-invoice" });Invoice invoice = qb.qbd().invoices().create( new InvoiceCreateInput("80000012-1730312001").refNumber("1042"), RequestOptions.create().idempotencyKey("order-5541-invoice"));What a repeated key returns
Section titled “What a repeated key returns”| 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.
Write outcomes
Section titled “Write outcomes”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.
Uncertain writes
Section titled “Uncertain writes”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:
- Look it up by
externalId. On creates that support it, we setexternalIdto a UUID derived from the request when you do not send one, and return it in the error’sdetails. Query the list (for exampleGET /v1/quickbooks-desktop/invoices?updatedAfter=...) and match onexternalId, or set your ownexternalIdon every create so you always know what to look for. - Or look it up by
refNumberorname. Invoices, bills and checks usually carry your reference number; list objects have unique names. - If you find it, use it. If you do not, create it again with a new idempotency key.
Safe retry checklist
Section titled “Safe retry checklist”- Let the SDK retry. It only retries what is safe and reuses the key.
- For raw HTTP, send an
Idempotency-Keyon everyPOSTandDELETE, and reuse it on retry. - Retry only when
retryableistrue(Daapi-Should-Retry: true), with backoff, honoringRetry-After. - On
pending, read the request. Onunknown, 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.