Skip to main content
See the API Reference for full documentation of all 261 QuickBooks operations and their REST(ish) endpoints, and even try them from right here in the docs. Better yet, use the MCP server to let your agent build and validate any request. Every QuickBooks operation is a REST endpoint under a connection, covering lists (customers, vendors, items, accounts), transactions (invoices, bills, payments, sales receipts, journal entries), payroll and reports. They are all compiled and validated directly from QuickBooks’ own schema definitions, so they support every feature native qbXML offers.
1

Send JSON

You send JSON to https://qubesync.com/api/v2/connections/{connection_id}/{resource}.
2

Save the queued request's ID

Your request is validated against QuickBooks’ schema. A valid request is queued, and you get 201 Created with the queued request.
3

Receive the answer

When the Web Connector next checks in, QuickBooks runs it and the answer is POSTed to your webhook_url as JSON.

The pattern

Fields are snake_case versions of QuickBooks’ own names (ListID becomes list_id, EditSequence becomes edit_sequence), in the request body and in the answer. Add webhook_url as a query parameter to every call, whatever the method. That’s where the answer goes.

Use a library

QuBe Sync’s libraries make these calls for you: the API key as basic auth, the response’s data out, an Idempotency-Key on every call that queues (yours, or one they make and resend on their own retries), safe retries after a timeout or a 503, and one APIError with the API’s status, code and message. A call and its retries stop after 60 seconds, and a retried DELETE answered 404 after a lost answer counts as done. They take the key as an argument, so your app reads it from the environment. They need Ruby 3.3, Python 3.8 or Node 16.17 (the webhook libraries’ table has the rest).
Every operation in the API reference is a method, named as workflows name it: the record’s kind, then the action (customer.create, invoice.query, txn_void.execute). Each one’s params are typed, with every field described: Ruby has RBS signatures, Python TypedDicts and Node .d.ts types. A kind’s code loads the first time your app uses it. The same client creates connections, starts workflow runs, answers their questions and reads your queued requests, a page at a time or all of them. Each library’s README lists every method.

A small API client

Without a library, every example here uses the same few lines: JSON in, the response’s data out.

Query

Filters are query parameters. Each resource has QuickBooks’ own filters, such as names, modified dates, active status and reference numbers: A query’s answer is always a list in response_json.data, even when one record or none matches. A list query QuickBooks pages arrives a page at a time, each page as its own webhook, unless you look records up by id or name or send max_returned alone. See Pagination.
Ask only for what changed, so a full import becomes a cheap incremental sync. The two kinds of query take the modified dates differently.A list query (customers, vendors, items…) takes from_modified_date and to_modified_date as plain query parameters.A transaction query (invoices, bills, payments…) takes them inside modified_date_range_filter.You can find full set of options in the API Reference, or use the MCP to build and validate requests.
Date fields take YYYY-MM-DD. Datetime fields take ISO 8601, such as 2026-09-03T17:28:28, with an optional offset such as +00:00. A trailing Z is accepted and sent to QuickBooks as +00:00. A value in any other shape is refused with 400 when you queue the request. A filter that takes several values, such as ref_number, list_id or txn_id, is an array. Repeat the key (ref_number=1001&ref_number=1002) or use brackets (ref_number[]=1001&ref_number[]=1002); either asks for every value you list. A comma doesn’t separate values: ref_number=1001,1002 looks for the one number 1001,1002. A filter that takes a single value, such as max_returned, can’t be repeated, and the request is refused with 400.

Create

The body is the record’s fields. Refer to associations by full_name or list_id: A create’s answer is the new record in response_json.data, with the ids QuickBooks gave it:
response_json (abridged)
Store the list_id or txn_id against your own record. A list_id is stable for the life of the company file; a full_name changes when someone renames the record in QuickBooks. Keep it with the connection it came from. QuickBooks makes a list_id unique only within one list family of one company file (customers, vendors, employees and other names share one; every item type shares another), and a txn_id within one transaction type: two of your customers’ files can hold the same one, and a copy of a file (a restored backup, a test copy) holds every one the same. So when your app keeps its own table of QuickBooks records, find each row by the connection, the kind and the id together: never by the id alone, and not by your customer’s account either, since a customer who connects another company file gets a new connection. If QuickBooks saved the record but left something out, such as a field this QuickBooks doesn’t support, the answer is still a success, with response_json.warning saying what. See Warnings.
Names are unique across QuickBooks’ customer, vendor, employee and other-name lists together. Creating a customer whose name a vendor already has fails with error 3100 (name_in_use). Plan for it: the ensure workflow handles it for you.

Update

QuickBooks uses optimistic locking. Send the record’s list_id (or txn_id) with the edit_sequence you last read, and preferably only the fields you’re changing: The answer is the updated record, with a new edit_sequence. If the record changed in QuickBooks since you read it, the update is refused with error 3200 (stale_edit_sequence): query the record again and retry with the fresh edit_sequence, or let your user decide how to manage conflicts. A workflow like upsert can handle this for you.

Requests are pre-validated

QuBe Sync validates every request against QuickBooks’ schema before queuing it, so a typo fails in your test suite, not on your customer’s PC. You get 400 and nothing is queued:
400 Bad Request
400 Bad Request

Ordering, dependencies and retries

Retrying a call safely

A timeout or a dropped connection can leave your app without the 201, and so without the request’s id, though the request was queued. Send an Idempotency-Key header with each call, and send a call whose answer you never got again with the same key:
string
1 to 255 printable ASCII characters. Every operation, queueing qbXML and starting a workflow run take one. Other calls ignore the header.A key belongs to your app, so your sandbox and production apps never share one. It lasts until what it created is purged, 60 days after the request is answered or the run ends (90 after a request fails), and then frees up: a retry after that queues something new.
The key covers a lost 201 from QuBe Sync, not a lost answer from QuickBooks. A request that ends timed_out was queued and sent, so sending it again with its key only returns it. See When the answer is lost.

Sending qbXML yourself

When no operation expresses the request you need, you already have the qbXML (from an older integration, or from Intuit’s own examples), or you want QuickBooks to run several requests together, queue the qbXML as it is:
string
required
The whole qbXML document, sent to QuickBooks as it is. It’s checked before anything is queued:
  1. It has no DOCTYPE. qbXML doesn’t use one.
  2. It declares its qbXML version, such as <?qbxml version="16.0"?>, and the version is 13.0, 14.0, 15.0 or 16.0.
  3. It is valid against that version’s qbXML schema.
  4. <QBXML> holds only <QBXMLMsgsRq>, with at least one request inside it.
  5. In a batch (two or more requests), no request has an iterator attribute. Queue an iterator query on its own.
  6. onError is stopOnError or continueOnError. QuickBooks Desktop refuses rollbackOnError for the whole envelope.
If it breaks one, nothing is queued and the answer is 422, with errors.request_xml saying which:
422 Unprocessable Entity
Write every amount with its two decimals: <TotalAmount>40.00</TotalAmount>, not 40. The schema allows 40, so it passes these checks, but QuickBooks refuses it with 3040 when it runs the request. Quantities and rates can be whole numbers. The operations write amounts this way for you.
string
Where the answer goes. It stays in the query string, as on every other call.
The answer arrives at your webhook_url like any other request. qbXML you send yourself is sent as you wrote it, so it isn’t paged for you. To page a single query, give it iterator="Start" and a MaxReturned page size, and you get one webhook per page of results. The 201 carries warnings beside data, a list that is empty when there’s nothing to say. A query QuickBooks pages that has neither an iterator nor MaxReturned, and doesn’t look records up by id or name, gets an unpaged_query warning: QuickBooks will answer it with every match at once, and one answer over the size limit isn’t kept. Warnings are only in that 201; links.self doesn’t return them.

A batch

Put several requests in one <QBXMLMsgsRq> and QuickBooks runs them in order, in one go. response_json then lists every request’s outcome, in the order you sent them:
response_json for a batch (abridged)

Handling the response

Create your requests with webhook urls to handle QuickBooks’ response!