Skip to content
Desktop Accounting API

Batch requests

Every QuickBooks Desktop request waits for the end user’s Web Connector and for QuickBooks to open the company file. When you need several things at once (create a customer, read a few invoices, run a report), send them as one batch: POST /v1/quickbooks-desktop/batch runs up to 25 typed operations together, in order, in one round trip, and returns one result per operation.

Each member names a typed operation in operationId and carries exactly what that operation’s own endpoint takes: pathParams for the path ({"id": "..."}), query for query parameters (as JSON values: numbers, booleans and arrays keep their types) and body for the JSON body.

Terminal window
curl -X POST https://api.desktopaccountingapi.com/v1/quickbooks-desktop/batch \
-H "Authorization: Bearer $DAAPI_SECRET_KEY" \
-H "Daapi-End-User-Id: $DAAPI_END_USER_ID" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"onError": "continueOnError",
"requests": [
{ "operationId": "qbd.customers.create", "body": { "name": "Acme Supply" } },
{ "operationId": "qbd.invoices.list", "query": { "customerIds": ["80000012-1730312001"], "limit": 20 } },
{ "operationId": "qbd.invoices.retrieve", "pathParams": { "id": "8000001A-1730312001" } }
]
}'

The response holds one result per member, in request order:

{
"objectType": "qbd_batch",
"onError": "continueOnError",
"outcome": "applied",
"results": [
{ "index": 0, "operationId": "qbd.customers.create", "status": "succeeded", "outcome": "applied", "data": { "id": "80000013-1730313000", "objectType": "qbd_customer", "name": "Acme Supply" }, "error": null, "warnings": [] },
{ "index": 1, "operationId": "qbd.invoices.list", "status": "succeeded", "outcome": "not_applicable", "data": { "objectType": "list", "url": "/v1/quickbooks-desktop/invoices", "data": [], "limitReached": false }, "error": null, "warnings": [] },
{ "index": 2, "operationId": "qbd.invoices.retrieve", "status": "failed", "outcome": "not_applicable", "data": null, "error": { "code": "QBD_OBJECT_NOT_FOUND", "param": "id" }, "warnings": [] }
]
}

(data and error are shortened here.) Each data is exactly what the operation returns on its own, and each error is the error it would return on its own.

The SDKs type every result by its operation. In TypeScript each result is typed by the request at the same position; in Python, C# and Java each result is an instance of the result class of its operation.

import { DesktopAccountingApi } from "@desktopaccountingapi/quickbooks-desktop";
const qb = new DesktopAccountingApi().forEndUser("eu_01j9...");
const batch = await qb.qbd.batch({
onError: "continueOnError",
requests: [
{ operationId: "qbd.customers.create", body: { name: "Acme Supply" } },
{ operationId: "qbd.invoices.list", query: { customerIds: ["80000012-1730312001"], limit: 20 } },
{ operationId: "qbd.invoices.retrieve", pathParams: { id: "8000001A-1730312001" } },
],
});
// Each result is typed by the request at the same position.
const [customer, invoices, invoice] = batch.results;
if (customer.status === "succeeded") console.log("created", customer.data?.id);
if (invoices.data) console.log(invoices.data.data.length, "invoices", invoices.data.limitReached ? "(more may match)" : "");
if (invoice.status !== "succeeded") console.log(invoice.error?.code);
  • Any typed operation. Every operation under /v1/quickbooks-desktop/ can be a member: lists, retrieves, creates, updates, deletes, voids, merges, custom fields and reports. The health check and passthrough cannot: send passthrough qbXML as one message set instead.
  • Validated like the endpoint. Each member is validated by the same schema and rules as its own endpoint before anything is queued. One invalid member fails the whole call with 400, and param names the member and field, for example requests[2].body.name or requests[0].query.limit. Nothing is sent.
  • Lists run as one query. A batch never starts a cursor: a cursor list runs as one query of at most limit records (default 150, at most 150), and its data is { objectType, url, data, limitReached }. limitReached is true when QuickBooks returned exactly limit records, so more may match: read the rest with the list endpoint and its cursor, or narrow the filters. cursor is refused, and ids, fullNames and refNumbers return every match, as they do on the list endpoint.
  • Updates that omit line collections. An update that sends some line arrays of a transaction and omits others first reads the transaction to keep the omitted lines, exactly as the update endpoint does (Updates and line items). That read is a separate request in the request log and runs before the batch is queued, so it needs the connection online.
  • At most 25 members. Each member’s response comes back in the same response, so keep large lists and reports to a few per batch.

results[i] answers requests[i]:

Field Meaning
index Position of the member in requests
operationId The member’s operation
status succeeded, failed, not_processed or unknown
outcome applied or not_applied for a write member, not_applicable for a read, unknown when the response was lost
data The operation’s typed result when status is succeeded, otherwise null
error The member’s error (code, message, fixes, param), otherwise null
warnings QuickBooks warnings and values read leniently for this member

onError decides what happens after a member fails:

  • stopOnError (default): QuickBooks skips every member after the first failure. Each skipped member has status: "not_processed" and the error QBD_REQUEST_NOT_PROCESSED (QuickBooks status 3231); nothing in it was applied. Use it when later members depend on earlier ones.
  • continueOnError: every member runs, whatever failed before it. Use it for independent work.

When QuickBooks answered the batch, the call returns 200 with every result, even if every member failed (the request in the request log is then failed with the first member’s error). Errors about the batch itself (validation, the connection, billing, timeouts) are returned as the call’s error, as for any request (Error handling).

A batch with at least one write member is a write: send one Idempotency-Key for the whole batch (the SDKs do this for you). A retry with the same key returns the stored results and never runs the batch twice; the same key with a different batch (other members, another order or onError) is refused with 422 IDEMPOTENCY_KEY_REUSED. QuickBooks receives all members as one message set with one message-set ID.

If the response from QuickBooks is lost (the Web Connector or the network failed while QuickBooks was working), it is unknown which members were applied. The call fails with 502 QBD_WRITE_OUTCOME_UNKNOWN, and error.details.members lists every member:

{ "index": 0, "operationId": "qbd.customers.create", "status": "unknown", "outcome": "unknown", "externalId": "0b9a0a54-2d93-4e2b-9f0a-3c3f2e1d4a10" }
{ "index": 1, "operationId": "qbd.invoices.retrieve", "status": "failed", "outcome": "not_applicable" }
  • Every write member is unknown. Never resend the batch with a new key. Check each write member in QuickBooks first: a create by its externalId (every create carries one), an update, delete or void by the object’s name or reference number (Idempotency and safe retries).
  • A read member is failed. A read in the same batch never proves that a write was applied, even when it ran.
  • Later writes on the connection wait until QuickBooks has finished with the batch, as for any write with an unknown outcome.
  • GET /v1/requests/{id} shows the same details.members. When the API later finds a create in QuickBooks by its externalId, that member shows status: "succeeded" with the record’s QuickBooks ID in objectId, while the others stay unknown.
  • Once every write member is found, the request becomes succeeded with recovered: true, and its result is the batch response: each create member is succeeded with data: null and the ID in recoveredObjectId. Retrieve the record by that ID. A replay with the same Idempotency-Key returns the same results and sends nothing to QuickBooks again; a read member in such a batch is failed (QBD_READ_INTERRUPTED), so send it again on its own.
  • Async mode. With Prefer: respond-async the call returns 202 with the request resource; its result is the batch response once QuickBooks has answered (Request lifecycle).
  • Read-only keys may batch reads only. A batch with a write member is refused with 403 API_KEY_READ_ONLY, and param names the first write member.
  • Timeouts apply to the whole batch: Daapi-Timeout-Seconds is how long the call waits for QuickBooks to answer every member.
  • Rate limits. A batch is one request for the rate limits and one entry in the connection’s queue.

A batch is billed like the same requests sent one by one. A production company file becomes active for the day when at least one member succeeds; a batch whose members all fail adds nothing. Health checks are not batch members, sample company files and test projects stay free, and a billing block refuses a production batch before anything is queued, as for any data request (Pricing and billing).

The MCP server runs batches through invoke_api_endpoint with the endpoint qbd.batch. A read-only MCP connection may batch reads only (MCP server).