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.
Send a batch
Section titled “Send a batch”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.
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);from desktopaccountingapi import DesktopAccountingApifrom desktopaccountingapi.types import ( BatchCustomerCreateInput, BatchCustomerCreateResult, BatchInvoiceListInput, BatchInvoiceListInputQuery, BatchInvoiceListResult, BatchInvoiceRetrieveInput, BatchPathIdInput, CustomerCreateInput,)
qb = DesktopAccountingApi().for_end_user("eu_01j9...")
batch = qb.qbd.batch( on_error="continueOnError", requests=[ BatchCustomerCreateInput(body=CustomerCreateInput(name="Acme Supply")), BatchInvoiceListInput(query=BatchInvoiceListInputQuery(customer_ids=["80000012-1730312001"], limit=20)), BatchInvoiceRetrieveInput(path_params=BatchPathIdInput(id="8000001A-1730312001")), ],)
# Each result is an instance of its operation's result class.for result in batch.results: if isinstance(result, BatchCustomerCreateResult) and result.data is not None: print("created", result.data.id) elif isinstance(result, BatchInvoiceListResult) and result.data is not None: print(len(result.data.data), "invoices", "(more may match)" if result.data.limit_reached else "") else: print(result.index, result.operation_id, result.status, result.error.code if result.error else None)var batch = await qb.Qbd.BatchAsync(new BatchInput{ OnError = BatchInputOnError.ContinueOnError, Requests = [ new BatchCustomerCreateInput { Body = new CustomerCreateInput { Name = "Acme Supply" } }, new BatchInvoiceListInput { Query = new BatchInvoiceListInputQuery { CustomerIds = ["80000012-1730312001"], Limit = 20 } }, new BatchInvoiceRetrieveInput { PathParams = new BatchPathIdInput { Id = "8000001A-1730312001" } }, ],});
// Each result is an instance of its operation's result class.foreach (var result in batch.Results){ switch (result) { case BatchCustomerCreateResult { Data: { } customer }: Console.WriteLine($"created {customer.Id}"); break; case BatchInvoiceListResult { Data: { } invoices }: Console.WriteLine($"{invoices.Data.Count} invoices{(invoices.LimitReached ? " (more may match)" : "")}"); break; default: Console.WriteLine($"{result.Index} {result.OperationId}: {result.Status} {result.Error?.Code}"); break; }}Batch batch = qb.qbd().batch(new BatchInput() .onError("continueOnError") .requests(List.of( new BatchCustomerCreateInput().body(new CustomerCreateInput("Acme Supply")), new BatchInvoiceListInput().query(new BatchInvoiceListInputQuery().customerIds(List.of("80000012-1730312001")).limit(20)), new BatchInvoiceRetrieveInput(new BatchPathIdInput("8000001A-1730312001")))));
// Each result is an instance of its operation's result class.for (BatchResult result : batch.results()) { if (result instanceof BatchCustomerCreateResult && ((BatchCustomerCreateResult) result).data() != null) { System.out.println("created " + ((BatchCustomerCreateResult) result).data().id()); } else if (result instanceof BatchInvoiceListResult && ((BatchInvoiceListResult) result).data() != null) { BatchInvoiceListPage invoices = ((BatchInvoiceListResult) result).data(); System.out.println(invoices.data().size() + " invoices" + (invoices.limitReached() ? " (more may match)" : "")); } else { System.out.println(result.index() + " " + result.operationId() + ": " + result.status()); }}Members
Section titled “Members”- 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, andparamnames the member and field, for examplerequests[2].body.nameorrequests[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
limitrecords (default 150, at most 150), and itsdatais{ objectType, url, data, limitReached }.limitReachedistruewhen QuickBooks returned exactlylimitrecords, so more may match: read the rest with the list endpoint and its cursor, or narrow the filters.cursoris refused, andids,fullNamesandrefNumbersreturn 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 and errors
Section titled “Results and errors”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 hasstatus: "not_processed"and the errorQBD_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).
Writes and safe retries
Section titled “Writes and safe retries”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 itsexternalId(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 samedetails.members. When the API later finds a create in QuickBooks by itsexternalId, that member showsstatus: "succeeded"with the record’s QuickBooks ID inobjectId, while the others stayunknown.- Once every write member is found, the request becomes
succeededwithrecovered: true, and itsresultis the batch response: each create member issucceededwithdata: nulland the ID inrecoveredObjectId. Retrieve the record by that ID. A replay with the sameIdempotency-Keyreturns the same results and sends nothing to QuickBooks again; a read member in such a batch isfailed(QBD_READ_INTERRUPTED), so send it again on its own.
Async mode, read-only keys and limits
Section titled “Async mode, read-only keys and limits”- Async mode. With
Prefer: respond-asyncthe call returns202with the request resource; itsresultis 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, andparamnames the first write member. - Timeouts apply to the whole batch:
Daapi-Timeout-Secondsis 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.
Billing
Section titled “Billing”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).
With AI agents
Section titled “With AI agents”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).