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’sdata 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).
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’sdata 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 inresponse_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.
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 byfull_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)
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.
Update
QuickBooks uses optimistic locking. Send the record’slist_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 get400 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 the201, 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:Write every amount with its two decimals:
- It has no DOCTYPE. qbXML doesn’t use one.
- It declares its qbXML version, such as
<?qbxml version="16.0"?>, and the version is 13.0, 14.0, 15.0 or 16.0. - It is valid against that version’s qbXML schema.
<QBXML>holds only<QBXMLMsgsRq>, with at least one request inside it.- In a batch (two or more requests), no request has an
iteratorattribute. Queue an iterator query on its own. onErrorisstopOnErrororcontinueOnError. QuickBooks Desktop refusesrollbackOnErrorfor the whole envelope.
422, with errors.request_xml saying which:422 Unprocessable Entity
<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.
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)

