qubesync 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +324 -2
  4. package/client.js +660 -0
  5. package/index.d.ts +367 -0
  6. package/index.js +409 -0
  7. package/index.mjs +22 -0
  8. package/operations/account.d.ts +263 -0
  9. package/operations/account.js +50 -0
  10. package/operations/account_tax_line_info.d.ts +31 -0
  11. package/operations/account_tax_line_info.js +26 -0
  12. package/operations/aging_report.d.ts +100 -0
  13. package/operations/aging_report.js +26 -0
  14. package/operations/alert.d.ts +118 -0
  15. package/operations/alert.js +50 -0
  16. package/operations/ar_refund_credit_card.d.ts +201 -0
  17. package/operations/ar_refund_credit_card.js +38 -0
  18. package/operations/bar_code.d.ts +40 -0
  19. package/operations/bar_code.js +26 -0
  20. package/operations/bill.d.ts +612 -0
  21. package/operations/bill.js +50 -0
  22. package/operations/bill_payment_check.d.ts +276 -0
  23. package/operations/bill_payment_check.js +50 -0
  24. package/operations/bill_payment_credit_card.d.ts +178 -0
  25. package/operations/bill_payment_credit_card.js +38 -0
  26. package/operations/bill_to_pay.d.ts +54 -0
  27. package/operations/bill_to_pay.js +26 -0
  28. package/operations/billing_rate.d.ts +159 -0
  29. package/operations/billing_rate.js +38 -0
  30. package/operations/budget_summary_report.d.ts +71 -0
  31. package/operations/budget_summary_report.js +26 -0
  32. package/operations/build_assembly.d.ts +264 -0
  33. package/operations/build_assembly.js +50 -0
  34. package/operations/charge.d.ts +330 -0
  35. package/operations/charge.js +50 -0
  36. package/operations/check.d.ts +394 -0
  37. package/operations/check.js +50 -0
  38. package/operations/class.d.ts +170 -0
  39. package/operations/class.js +50 -0
  40. package/operations/cleared_status.d.ts +43 -0
  41. package/operations/cleared_status.js +26 -0
  42. package/operations/client.d.ts +254 -0
  43. package/operations/company.d.ts +36 -0
  44. package/operations/company.js +26 -0
  45. package/operations/company_activity.d.ts +32 -0
  46. package/operations/company_activity.js +26 -0
  47. package/operations/credit_card_charge.d.ts +280 -0
  48. package/operations/credit_card_charge.js +50 -0
  49. package/operations/credit_card_credit.d.ts +280 -0
  50. package/operations/credit_card_credit.js +50 -0
  51. package/operations/credit_memo.d.ts +642 -0
  52. package/operations/credit_memo.js +50 -0
  53. package/operations/currency.d.ts +195 -0
  54. package/operations/currency.js +50 -0
  55. package/operations/custom_detail_report.d.ts +108 -0
  56. package/operations/custom_detail_report.js +26 -0
  57. package/operations/custom_summary_report.d.ts +119 -0
  58. package/operations/custom_summary_report.js +26 -0
  59. package/operations/customer.d.ts +639 -0
  60. package/operations/customer.js +50 -0
  61. package/operations/customer_msg.d.ts +121 -0
  62. package/operations/customer_msg.js +38 -0
  63. package/operations/customer_type.d.ts +127 -0
  64. package/operations/customer_type.js +38 -0
  65. package/operations/data_event_recovery_info.d.ts +61 -0
  66. package/operations/data_event_recovery_info.js +38 -0
  67. package/operations/data_ext.d.ts +197 -0
  68. package/operations/data_ext.js +50 -0
  69. package/operations/data_ext_def.d.ts +173 -0
  70. package/operations/data_ext_def.js +62 -0
  71. package/operations/date_driven_terms.d.ts +141 -0
  72. package/operations/date_driven_terms.js +38 -0
  73. package/operations/deposit.d.ts +402 -0
  74. package/operations/deposit.js +50 -0
  75. package/operations/employee.d.ts +683 -0
  76. package/operations/employee.js +50 -0
  77. package/operations/employee_default.d.ts +133 -0
  78. package/operations/employee_default.js +38 -0
  79. package/operations/entity.d.ts +96 -0
  80. package/operations/entity.js +26 -0
  81. package/operations/estimate.d.ts +590 -0
  82. package/operations/estimate.js +50 -0
  83. package/operations/form1099_category_account_mapping.d.ts +89 -0
  84. package/operations/form1099_category_account_mapping.js +38 -0
  85. package/operations/general_detail_report.d.ts +108 -0
  86. package/operations/general_detail_report.js +26 -0
  87. package/operations/general_summary_report.d.ts +115 -0
  88. package/operations/general_summary_report.js +26 -0
  89. package/operations/host.d.ts +43 -0
  90. package/operations/host.js +26 -0
  91. package/operations/index.js +397 -0
  92. package/operations/inventory_adjustment.d.ts +389 -0
  93. package/operations/inventory_adjustment.js +50 -0
  94. package/operations/inventory_site.d.ts +247 -0
  95. package/operations/inventory_site.js +50 -0
  96. package/operations/invoice.d.ts +682 -0
  97. package/operations/invoice.js +50 -0
  98. package/operations/item.d.ts +100 -0
  99. package/operations/item.js +26 -0
  100. package/operations/item_assemblies_can_build.d.ts +41 -0
  101. package/operations/item_assemblies_can_build.js +26 -0
  102. package/operations/item_discount.d.ts +256 -0
  103. package/operations/item_discount.js +50 -0
  104. package/operations/item_fixed_asset.d.ts +382 -0
  105. package/operations/item_fixed_asset.js +50 -0
  106. package/operations/item_group.d.ts +236 -0
  107. package/operations/item_group.js +50 -0
  108. package/operations/item_inventory.d.ts +350 -0
  109. package/operations/item_inventory.js +50 -0
  110. package/operations/item_inventory_assembly.d.ts +368 -0
  111. package/operations/item_inventory_assembly.js +50 -0
  112. package/operations/item_non_inventory.d.ts +264 -0
  113. package/operations/item_non_inventory.js +50 -0
  114. package/operations/item_other_charge.d.ts +244 -0
  115. package/operations/item_other_charge.js +50 -0
  116. package/operations/item_payment.d.ts +220 -0
  117. package/operations/item_payment.js +50 -0
  118. package/operations/item_receipt.d.ts +362 -0
  119. package/operations/item_receipt.js +50 -0
  120. package/operations/item_sales_tax.d.ts +228 -0
  121. package/operations/item_sales_tax.js +50 -0
  122. package/operations/item_sales_tax_group.d.ts +192 -0
  123. package/operations/item_sales_tax_group.js +50 -0
  124. package/operations/item_service.d.ts +256 -0
  125. package/operations/item_service.js +50 -0
  126. package/operations/item_sites.d.ts +81 -0
  127. package/operations/item_sites.js +26 -0
  128. package/operations/item_subtotal.d.ts +188 -0
  129. package/operations/item_subtotal.js +50 -0
  130. package/operations/job_report.d.ts +99 -0
  131. package/operations/job_report.js +26 -0
  132. package/operations/job_type.d.ts +127 -0
  133. package/operations/job_type.js +38 -0
  134. package/operations/journal_entry.d.ts +339 -0
  135. package/operations/journal_entry.js +50 -0
  136. package/operations/lead.d.ts +291 -0
  137. package/operations/lead.js +50 -0
  138. package/operations/list.d.ts +39 -0
  139. package/operations/list.js +26 -0
  140. package/operations/list_deleted.d.ts +44 -0
  141. package/operations/list_deleted.js +26 -0
  142. package/operations/list_display.d.ts +65 -0
  143. package/operations/list_display.js +38 -0
  144. package/operations/list_merge.d.ts +58 -0
  145. package/operations/list_merge.js +26 -0
  146. package/operations/other_name.d.ts +280 -0
  147. package/operations/other_name.js +50 -0
  148. package/operations/paycheck.d.ts +82 -0
  149. package/operations/paycheck.js +26 -0
  150. package/operations/payment_method.d.ts +131 -0
  151. package/operations/payment_method.js +38 -0
  152. package/operations/payroll_detail_report.d.ts +97 -0
  153. package/operations/payroll_detail_report.js +26 -0
  154. package/operations/payroll_item.d.ts +83 -0
  155. package/operations/payroll_item.js +26 -0
  156. package/operations/payroll_item_addition.d.ts +95 -0
  157. package/operations/payroll_item_addition.js +38 -0
  158. package/operations/payroll_item_company_contribution.d.ts +105 -0
  159. package/operations/payroll_item_company_contribution.js +38 -0
  160. package/operations/payroll_item_deduction.d.ts +101 -0
  161. package/operations/payroll_item_deduction.js +38 -0
  162. package/operations/payroll_item_direct_deposit.d.ts +65 -0
  163. package/operations/payroll_item_direct_deposit.js +38 -0
  164. package/operations/payroll_item_non_wage.d.ts +92 -0
  165. package/operations/payroll_item_non_wage.js +26 -0
  166. package/operations/payroll_item_tax.d.ts +108 -0
  167. package/operations/payroll_item_tax.js +38 -0
  168. package/operations/payroll_item_wage.d.ts +169 -0
  169. package/operations/payroll_item_wage.js +50 -0
  170. package/operations/payroll_item_workers_compensation.d.ts +77 -0
  171. package/operations/payroll_item_workers_compensation.js +38 -0
  172. package/operations/payroll_last_period.d.ts +31 -0
  173. package/operations/payroll_last_period.js +26 -0
  174. package/operations/payroll_liability_adjustment.d.ts +137 -0
  175. package/operations/payroll_liability_adjustment.js +38 -0
  176. package/operations/payroll_liability_check.d.ts +82 -0
  177. package/operations/payroll_liability_check.js +26 -0
  178. package/operations/payroll_prior_payment.d.ts +126 -0
  179. package/operations/payroll_prior_payment.js +38 -0
  180. package/operations/payroll_summary_report.d.ts +107 -0
  181. package/operations/payroll_summary_report.js +26 -0
  182. package/operations/payroll_year_to_date_adjustment.d.ts +156 -0
  183. package/operations/payroll_year_to_date_adjustment.js +38 -0
  184. package/operations/preferences.d.ts +64 -0
  185. package/operations/preferences.js +38 -0
  186. package/operations/price_level.d.ts +234 -0
  187. package/operations/price_level.js +50 -0
  188. package/operations/purchase_order.d.ts +620 -0
  189. package/operations/purchase_order.js +50 -0
  190. package/operations/qb_command_exec.d.ts +49 -0
  191. package/operations/qb_command_exec.js +26 -0
  192. package/operations/receive_payment.d.ts +450 -0
  193. package/operations/receive_payment.js +50 -0
  194. package/operations/receive_payment_to_deposit.d.ts +36 -0
  195. package/operations/receive_payment_to_deposit.js +26 -0
  196. package/operations/sales_order.d.ts +612 -0
  197. package/operations/sales_order.js +50 -0
  198. package/operations/sales_receipt.d.ts +650 -0
  199. package/operations/sales_receipt.js +50 -0
  200. package/operations/sales_rep.d.ts +170 -0
  201. package/operations/sales_rep.js +50 -0
  202. package/operations/sales_tax_code.d.ts +194 -0
  203. package/operations/sales_tax_code.js +50 -0
  204. package/operations/sales_tax_payable.d.ts +42 -0
  205. package/operations/sales_tax_payable.js +26 -0
  206. package/operations/sales_tax_payment_check.d.ts +254 -0
  207. package/operations/sales_tax_payment_check.js +50 -0
  208. package/operations/sales_tax_return.d.ts +35 -0
  209. package/operations/sales_tax_return.js +26 -0
  210. package/operations/sales_tax_return_line.d.ts +36 -0
  211. package/operations/sales_tax_return_line.js +26 -0
  212. package/operations/ship_method.d.ts +121 -0
  213. package/operations/ship_method.js +38 -0
  214. package/operations/special_account.d.ts +40 -0
  215. package/operations/special_account.js +26 -0
  216. package/operations/special_item.d.ts +44 -0
  217. package/operations/special_item.js +26 -0
  218. package/operations/standard_terms.d.ts +133 -0
  219. package/operations/standard_terms.js +38 -0
  220. package/operations/template.d.ts +36 -0
  221. package/operations/template.js +26 -0
  222. package/operations/terms.d.ts +90 -0
  223. package/operations/terms.js +26 -0
  224. package/operations/time_report.d.ts +84 -0
  225. package/operations/time_report.js +26 -0
  226. package/operations/time_tracking.d.ts +214 -0
  227. package/operations/time_tracking.js +50 -0
  228. package/operations/to_do.d.ts +254 -0
  229. package/operations/to_do.js +50 -0
  230. package/operations/transaction.d.ts +244 -0
  231. package/operations/transaction.js +26 -0
  232. package/operations/transfer.d.ts +178 -0
  233. package/operations/transfer.js +50 -0
  234. package/operations/transfer_inventory.d.ts +290 -0
  235. package/operations/transfer_inventory.js +50 -0
  236. package/operations/txn.d.ts +39 -0
  237. package/operations/txn.js +26 -0
  238. package/operations/txn_deleted.d.ts +44 -0
  239. package/operations/txn_deleted.js +26 -0
  240. package/operations/txn_display.d.ts +69 -0
  241. package/operations/txn_display.js +38 -0
  242. package/operations/txn_void.d.ts +39 -0
  243. package/operations/txn_void.js +26 -0
  244. package/operations/types.d.ts +1259 -0
  245. package/operations/unit_of_measure_set.d.ts +168 -0
  246. package/operations/unit_of_measure_set.js +38 -0
  247. package/operations/vehicle.d.ts +168 -0
  248. package/operations/vehicle.js +50 -0
  249. package/operations/vehicle_mileage.d.ts +147 -0
  250. package/operations/vehicle_mileage.js +38 -0
  251. package/operations/vendor.d.ts +541 -0
  252. package/operations/vendor.js +50 -0
  253. package/operations/vendor_credit.d.ts +365 -0
  254. package/operations/vendor_credit.js +50 -0
  255. package/operations/vendor_type.d.ts +127 -0
  256. package/operations/vendor_type.js +38 -0
  257. package/operations/workers_comp_code.d.ts +199 -0
  258. package/operations/workers_comp_code.js +50 -0
  259. package/package.json +52 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (2026-10-25)
4
+
5
+ The v2 API client.
6
+
7
+ - `new Client({ apiKey, baseUrl = "https://qubesync.com", connectTimeout = 5000, timeout = 30000, maxRetries = 2 })`:
8
+ Basic auth with the key passed in (the environment is never read), and the key never in
9
+ `util.inspect`, `JSON.stringify` or an error.
10
+ - Connections: `createConnection`, `getConnection`, `listConnections`, `updateConnection`,
11
+ `deleteConnection`, `regenerateOnboarding`, `setPassword`, `getQwc`.
12
+ - Runs: `startRun` (the `reference` is the Idempotency-Key), `getRun` by link or ids, `listRuns`,
13
+ `cancelRun`, `runEvents`. Questions: `answer`, `search`.
14
+ - Every QuickBooks operation as a method, entity then action: `qube.customer.create(connectionId,
15
+ params, { webhookUrl, idempotencyKey })`, `qube.customerMsg.query(...)`, `qube.txnVoid.execute(...)`
16
+ (261 operations on 124 entities). Each entity's module loads on first use, and can be imported
17
+ alone (`qubesync/operations/<entity>`). Generated from the QuBe Sync operations manifest
18
+ (`test/fixtures/operations.json`, served at https://qubesync.com/libraries/operations.json) by
19
+ `node scripts/generate-operations.js`, with full nested param types and every field's description
20
+ in `operations/*.d.ts`; CI fails when the generated files or the vendored manifest drift.
21
+ - Query params: nested objects go bracketed (`name_range[from]=A`), arrays of scalars as a repeated key.
22
+ - `queue(operationId, connectionId, params, { webhookUrl, idempotencyKey, method, path })` queues by
23
+ operation id; an id this version doesn't know needs `method` and `path`.
24
+ - Requests: `queueQbxml`, `getRequest`, `requestPages`, `listRequests`, `eachRequest` (an async
25
+ iterator over every page), `discardRequest`; `request` for anything else.
26
+ - Links are refused unless they're on the client's own origin.
27
+ - Retries for retry-safe requests on a failed connection, a timeout, 429, 502, 503 and 504, with
28
+ `Retry-After` honoured up to 30 s. `answer` is retried when its link has a `prompt_token`.
29
+ - `totalTimeout` (60000 ms): a call returns or rejects within it, retries and waits included; each
30
+ attempt's timeouts are cut to the time left, and a retry that would leave under 1 s isn't made.
31
+ - `onRetry({ attempt, maxRetries, delayMs, elapsedMs, method, url, status, error })` before each
32
+ retry's wait; the URL without its query, never the key; a throw stops the call.
33
+ - A retried DELETE that gets a 404 after an earlier attempt's answer was lost (a read timeout, a
34
+ reset after connecting, a 502 or 504) resolves to `null`, as a 204 would.
35
+ - `APIError` with `status`, `code`, `message`, `errors`, `body` and `requestId`.
36
+ - Releases publish through npm trusted publishing, from `release.yml`'s OIDC token; no npm
37
+ token is stored.
38
+
39
+ ## 0.1.0 (2026-10-24)
40
+
41
+ First version: webhooks only.
42
+
43
+ - `verify(rawBody, header, { secrets, tolerance = 300, now })` checks the `X-QUBE-Signature`
44
+ header (HMAC-SHA256 of `"<t>." + raw body`, constant-time compare, one or more secrets while
45
+ one rotates) and returns a typed event, or throws `SignatureError` with `reason` `missing`,
46
+ `malformed`, `stale` or `mismatch`.
47
+ - `parse(rawBody)` builds the typed event without verifying.
48
+ - Events: `RequestEvent`, `WorkflowRunEvent`, `NotificationEvent`, `ConnectionEvent`,
49
+ `NameIndexEvent` and `UnknownEvent`, each with `type`, `event`, `timestamp`, `appId`,
50
+ `sandbox`, `body`, `revision`, `reference` and `dedupeKey`; `newerThan(storedRevision)` on
51
+ workflow runs and connections.
52
+ - CommonJS and ESM entry points, TypeScript declarations, zero dependencies, Node 16.17 and later.
53
+ - Tested against the server's own test vectors (`test/fixtures/test_vectors.json`), and CI checks
54
+ that copy against https://qubesync.com/webhooks/test_vectors.json.
55
+ - A `v*` tag publishes to npm (`release.yml`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QuBe Sync
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,325 @@
1
- # Temporary Holding Version
1
+ # qubesync (Node)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The [QuBe Sync](https://qubesync.com) v2 API client, and webhook verification and parsing:
4
+ QuickBooks Desktop through qubesync.com. Plain JavaScript, zero dependencies, Node 16.17 and
5
+ later, CommonJS and ESM, TypeScript declarations included. API docs: https://docs.qubesync.com.
6
+
7
+ ```bash
8
+ npm install qubesync
9
+ ```
10
+
11
+ ## The v2 client
12
+
13
+ Make one client with your app's API key. The library never reads the environment, so pass the
14
+ key in (and hold a sandbox and a production client at once if you need both):
15
+
16
+ ```js
17
+ const { Client, APIError } = require("qubesync"); // or: import { Client, APIError } from "qubesync";
18
+
19
+ const qube = new Client({ apiKey: process.env.QUBE_API_KEY });
20
+ ```
21
+
22
+ `new Client({ apiKey, baseUrl = "https://qubesync.com", connectTimeout = 5000, timeout = 30000, maxRetries = 2, totalTimeout = 60000, onRetry })`.
23
+ Timeouts are in milliseconds: `connectTimeout` to open the connection, `timeout` for the socket to
24
+ sit idle waiting for the answer, and `totalTimeout` for the whole call (see below). Every method returns a Promise of the response's `data`, with
25
+ the API's own snake_case keys (`webhook_url`, `total_pages`), or `null` for an empty answer.
26
+
27
+ ### Onboarding a customer
28
+
29
+ ```js
30
+ const connection = await qube.createConnection({ webhookUrl: "https://app.example.com/webhooks/qube", name: "Acme" });
31
+ // Store connection.id, and send your customer to connection.links.onboarding.
32
+ // A connection.connected webhook arrives once their Web Connector checks in.
33
+
34
+ const fresh = await qube.regenerateOnboarding(connection.id); // a new onboarding link
35
+ ```
36
+
37
+ Also `getConnection(id)`, `listConnections()`, `updateConnection(id, { webhookUrl, name })`,
38
+ `deleteConnection(id)`, and for setup without hosted onboarding `setPassword(id, { password })`
39
+ (leave `password` out to have one generated) and `getQwc(id)` (the QWC file's XML, a string).
40
+
41
+ ### Starting a run and answering its question
42
+
43
+ ```js
44
+ const run = await qube.startRun(connectionId, "customer_sync", { customer: { name: "Acme" } }, {
45
+ reference: "customer-1042", // your own id for the run, and its Idempotency-Key
46
+ webhookUrl: "https://app.example.com/webhooks/qube",
47
+ });
48
+
49
+ // Later, in the workflow_run.awaiting_input webhook (event.prompt), answer by choice:
50
+ await qube.answer(event.prompt.links.decisions, { choice: "use_existing", data: { customer: "80000001-1700000000" } });
51
+ // A question's record field: search QuickBooks with the prompt's search link.
52
+ const matches = await qube.search(event.prompt.links.search, { field: "customer", q: "Acm" });
53
+ ```
54
+
55
+ Also `getRun(runUrl)` (a run's `links.run`) or `getRun(connectionId, runId)`,
56
+ `listRuns(connectionId, { reference, state, outcome })`, `cancelRun(run.links.cancel, { reason })` and
57
+ `runEvents(connectionId, runId, { after, limit })`, which returns the whole body,
58
+ `{ data, next_after }`.
59
+
60
+ Links (`links.run`, `links.decisions`, `links.search`, `links.cancel`) are used as they come, query
61
+ string included. A link on another origin than the client's `baseUrl` (scheme, host and port) is
62
+ refused with a `TypeError` before any request, so the API key only ever goes to QuBe Sync.
63
+
64
+ ### Queueing an operation
65
+
66
+ Every QuickBooks operation is a method, entity then action: `create`, `update`, `query`, `delete` or
67
+ `execute`. Each takes the connection id, the params (the API's own snake_case fields) and
68
+ `{ webhookUrl, idempotencyKey }`, and resolves to the queued request; the answer arrives at
69
+ `webhookUrl`.
70
+
71
+ ```js
72
+ // POST /connections/{id}/customers
73
+ const queued = await qube.customer.create(connectionId, {
74
+ name: "Acme",
75
+ bill_address: { addr1: "1 Main St", city: "Austin", state: "TX", postal_code: "78701" },
76
+ }, { webhookUrl: "https://app.example.com/webhooks/qube" });
77
+
78
+ // GET /connections/{id}/invoices: a query's params go in the query string.
79
+ await qube.invoice.query(connectionId, {
80
+ modified_date_range_filter: { from_modified_date: "2026-10-01T00:00:00Z" },
81
+ include_line_items: true,
82
+ }, { webhookUrl: "https://app.example.com/webhooks/qube" });
83
+
84
+ await qube.customerMsg.query(connectionId, {}, { webhookUrl: "https://app.example.com/webhooks/qube" });
85
+ await qube.txnVoid.execute(connectionId, { txn_id: "1A2B-1700000000", txn_void_type: "Invoice" }, {
86
+ webhookUrl: "https://app.example.com/webhooks/qube",
87
+ });
88
+ ```
89
+
90
+ Each entity's module loads the first time you use it (`qube.customer`), so the 124 of them cost
91
+ nothing until then. A bundled app can import one alone:
92
+ `const { Customer } = require("qubesync/operations/customer"); new Customer(qube).create(...)`.
93
+
94
+ The types are in `operations/<entity>.d.ts` (and nested ones shared between entities in
95
+ `operations/types.d.ts`): every field with its description, required where QuickBooks requires it,
96
+ enums as literal unions, so an editor completes and checks the params. Each method's doc says whether
97
+ QuickBooks answers in pages (each page comes as its own webhook) and links its
98
+ [API reference](https://docs.qubesync.com) page. They're generated from the QuBe Sync operations
99
+ manifest (`node scripts/generate-operations.js`), not written by hand.
100
+
101
+ `queue(operationId, connectionId, params, { webhookUrl, idempotencyKey, method, path })` queues an
102
+ operation by its id, the same as the method: `qube.queue("customer.create", connectionId, params, options)`.
103
+ For an operation newer than this version, pass `method` and `path` (relative to the connection):
104
+ `qube.queue("widget.create", connectionId, params, { webhookUrl, method: "POST", path: "widgets" })`.
105
+
106
+ qbXML as written:
107
+
108
+ ```js
109
+ await qube.queueQbxml(connectionId, requestXml, { webhookUrl: "https://app.example.com/webhooks/qube" });
110
+ ```
111
+
112
+ Also `getRequest(connectionId, id)`, `requestPages(connectionId, id)`, `discardRequest(connectionId, id)`,
113
+ and `listRequests(connectionId, { page, pageSize, state, webhookState, sort, sortDirection, searchPhrase })`,
114
+ which returns the whole body, `{ data, meta: { page, total_pages } }`. To walk every page:
115
+
116
+ ```js
117
+ for await (const request of qube.eachRequest(connectionId, { state: "errored" })) {
118
+ console.log(request.id);
119
+ }
120
+ ```
121
+
122
+ ### Anything else
123
+
124
+ `request(method, pathOrUrl, body, { query, headers })` sends any v2 call, a path under `/api/v2`
125
+ (`"/workflows"`) or a same-origin URL, and returns the parsed body whole.
126
+
127
+ ### Retries and idempotency
128
+
129
+ A request that's safe to send again (a GET, PUT or DELETE, or one with an `Idempotency-Key`) is
130
+ retried up to `maxRetries` times, 0.5 s then 1 s apart, when the connection fails or times out, or
131
+ QuBe Sync answers 429, 502, 503 or 504 (a 429 or 503's `Retry-After` is honoured, up to 30 s).
132
+
133
+ - `startRun` sends `reference` as the Idempotency-Key, so a retried start never starts two runs.
134
+ - Every operation method, `queue` and `queueQbxml` take `idempotencyKey`; left out, the client makes
135
+ a UUID per call and sends it again on that call's retries, so a retry never queues a request twice.
136
+ - `answer` is retried when its link has a `prompt_token` (a run's `links.decisions` always does
137
+ while it asks). A retry after a lost answer may reject with a `409` whose `code` is
138
+ `prompt_changed`: the question has been answered, by the first attempt or by someone else.
139
+ Without a `prompt_token` it's sent once, since a repeat could be taken for the run's next question.
140
+ - The other POSTs (`createConnection`, `cancelRun`, `setPassword`, `getQwc`,
141
+ `regenerateOnboarding`, `discardRequest`) are sent once.
142
+ - A retried DELETE (`deleteConnection`, or `request("DELETE", ...)`) that gets a `404` resolves to
143
+ `null`, as a 204 would, when an earlier attempt's answer was lost after the request could have
144
+ arrived (a read timeout, a connection reset after it opened, a 502 or 504): that attempt may have
145
+ deleted it. Any other 404 still rejects, including a retry's after a refused connection, a
146
+ connect timeout, a 429 or a 503.
147
+
148
+ A call returns or rejects within `totalTimeout` (60 s), retries and their waits included: each
149
+ attempt's timeouts are cut to the time left, and a retry is skipped (the last answer or error
150
+ rejects) when its wait, a `Retry-After` included, would leave under a second. Pass `maxRetries: 0`
151
+ for calls made while a user waits on a page, if 60 s is too long.
152
+
153
+ `onRetry` is called before each retry's wait with a frozen object: `attempt` (1, 2...),
154
+ `maxRetries`, `delayMs`, `elapsedMs`, `method`, `url` (without its query string), and `status` for
155
+ a retried status or `error` for a network failure or timeout (the other is `null`). It's never
156
+ given the API key, headers or body, and it isn't called when no retry is made. Anything it throws
157
+ stops the call, which rejects with that error:
158
+
159
+ ```js
160
+ const qube = new Client({
161
+ apiKey: process.env.QUBE_API_KEY,
162
+ onRetry: ({ attempt, maxRetries, delayMs, method, url, status, error }) =>
163
+ console.warn(`QuBe Sync retry ${attempt}/${maxRetries} in ${Math.round(delayMs)} ms: ${method} ${url} (${status ?? error.code})`),
164
+ });
165
+ ```
166
+
167
+ ### Errors
168
+
169
+ Any answer outside 2xx rejects with `APIError`:
170
+
171
+ ```js
172
+ try {
173
+ await qube.answer(decisionsUrl, { choice: "create" });
174
+ } catch (error) {
175
+ if (!(error instanceof APIError)) throw error; // Node's own error: the connection failed after the retries
176
+ error.status; // 409
177
+ error.code; // "prompt_changed", when errors carries a code
178
+ error.errors; // the body's errors as parsed: a sentence, { detail, code }, fields, or [{ path, message }]
179
+ error.message; // "QuBe Sync answered 409: ...", one line, safe to log
180
+ error.body; // the raw body
181
+ error.requestId; // the x-request-id header, for support
182
+ }
183
+ ```
184
+
185
+ A connection that still fails after the retries rejects with Node's own error (`ECONNREFUSED`,
186
+ `ECONNRESET`, or `ETIMEDOUT` for a timeout), unchanged. Neither ever holds the API key, and
187
+ `util.inspect(client)` and `JSON.stringify(client)` show only the `baseUrl`. See
188
+ [error handling](https://docs.qubesync.com/error_handling).
189
+
190
+ ## Webhooks
191
+
192
+ Every delivery carries an `X-QUBE-Signature` header: `t=<unix seconds>,v1=<hex>[,v1=<hex>...]`,
193
+ one `v1` per active secret while you rotate. `verify` checks it against the **raw** body, then
194
+ parses the body into a typed event:
195
+
196
+ ```js
197
+ const { verify, SignatureError } = require("qubesync"); // or: import { verify, SignatureError } from "qubesync";
198
+
199
+ const SECRETS = [process.env.QUBE_WEBHOOK_SECRET]; // add the new secret here while you rotate
200
+
201
+ function handleDelivery(rawBody, signatureHeader) {
202
+ let event;
203
+ try {
204
+ event = verify(rawBody, signatureHeader, { secrets: SECRETS });
205
+ } catch (error) {
206
+ if (error instanceof SignatureError) return 401; // error.reason: missing | malformed | stale | mismatch
207
+ throw error;
208
+ }
209
+
210
+ switch (event.type) {
211
+ case "request":
212
+ // A queued request's answer. Dedupe on (page_request_id, state): event.dedupeKey.
213
+ // A paginated query is done when you hold pages 1..N for the page_request_id.
214
+ if (alreadyProcessed(event.dedupeKey)) return 200;
215
+ saveAnswer(event.pageRequestId, event.page, event.state, event.responseJson, event.error);
216
+ break;
217
+
218
+ case "workflow_run":
219
+ // awaiting_input, completed, failed or cancelled. Deliveries can arrive out of order:
220
+ // apply one only when it's newer than the revision you stored for the run.
221
+ if (!event.newerThan(storedRunRevision(event.runId))) return 200;
222
+ saveRun(event.runId, event.reference, event.revision, event.runState, event.output, event.prompt);
223
+ break;
224
+
225
+ case "notification":
226
+ // Data a run sends along the way. Dedupe on event.dedupeKey (the delivery id);
227
+ // never skip one by revision.
228
+ if (alreadyProcessed(event.dedupeKey)) return 200;
229
+ saveNotification(event.runId, event.name, event.seq, event.data);
230
+ break;
231
+
232
+ case "connection":
233
+ // connected, setup_cancelled, stalled, resumed. Dedupe on the delivery id and apply
234
+ // only when newer than the connection's stored revision.
235
+ if (alreadyProcessed(event.dedupeKey) || !event.newerThan(storedConnectionRevision(event.connectionId))) return 200;
236
+ saveConnection(event.connectionId, event.event, event.revision, event.stallReason);
237
+ break;
238
+
239
+ case "name_index":
240
+ saveNameIndex(event.connectionId, event.nameIndex);
241
+ break;
242
+
243
+ default:
244
+ // "unknown": an event newer than this library. Accept it; event.body has everything.
245
+ break;
246
+ }
247
+ return 200;
248
+ }
249
+ ```
250
+
251
+ `verify(rawBody, header, { secrets, tolerance = 300, now })`:
252
+
253
+ - `rawBody` is the body exactly as received, as a `Buffer` or string (a string is signed as its
254
+ UTF-8 bytes). A parsed object throws a `TypeError`: re-serialized JSON never matches.
255
+ - `header` is the `X-QUBE-Signature` value. Node lowercases header names, so read
256
+ `req.headers["x-qube-signature"]`.
257
+ - `secrets` is one secret or an array of them. An empty or missing list throws a `TypeError`. The
258
+ library never reads the environment.
259
+ - `tolerance` is how many seconds the timestamp may be off either way. `now` is unix seconds or a
260
+ `Date` and defaults to the current time.
261
+ - On failure it throws `SignatureError`, whose `reason` is `missing`, `malformed`, `stale` or
262
+ `mismatch`, checked in that order. Its message is safe to log: it never holds the secret, the
263
+ signatures or the body.
264
+
265
+ `parse(rawBody)` builds the same typed event without verifying, for tests and for code that
266
+ verified elsewhere.
267
+
268
+ ### Events
269
+
270
+ Every event has `type`, `event` (or `null` for a request's answer), `timestamp`, `appId`,
271
+ `sandbox`, `revision`, `reference`, `dedupeKey` and `body` (the whole parsed body). Missing keys
272
+ read as `null`; nothing throws.
273
+
274
+ | `type` | when | also |
275
+ | --- | --- | --- |
276
+ | `request` | no `event`, has `page_request_id` | `id`, `pageRequestId`, `state`, `page`, `iterationState`, `responseJson`, `error`; `dedupeKey` is `"<page_request_id>:<state>"` |
277
+ | `workflow_run` | `workflow_run.awaiting_input`, `.completed`, `.failed`, `.cancelled` | `runId`, `reference`, `revision`, `runState`, `workflow`, `connectionId`, `prompt`, `outcome`, `output`, `error`, `summary`, `cancelReason`, `links`, `newerThan(stored)` |
278
+ | `notification` | `workflow_run.notification` | `runId`, `reference`, `revision`, `deliveryId`, `seq`, `name`, `state`, `from`, `data`; `dedupeKey` is the delivery id; no `newerThan` |
279
+ | `connection` | `connection.*` | `deliveryId`, `revision`, `connection`, `connectionId`, `stallReason`, `newerThan(stored)`; `dedupeKey` is the delivery id |
280
+ | `name_index` | `name_index.*` | `connectionId`, `nameIndex` |
281
+ | `unknown` | anything else | the common fields only |
282
+
283
+ `newerThan(stored)` is `true` when `stored` is `null` or `undefined`, else `revision > stored`.
284
+
285
+ ### Express
286
+
287
+ Verify before any JSON body parser touches the request: mount `express.raw` on the webhook route
288
+ so `req.body` is the raw `Buffer`.
289
+
290
+ ```js
291
+ const express = require("express");
292
+ const { verify, SignatureError } = require("qubesync");
293
+
294
+ const app = express();
295
+
296
+ app.post("/webhooks/qube", express.raw({ type: "application/json" }), (req, res) => {
297
+ let event;
298
+ try {
299
+ event = verify(req.body, req.get("X-QUBE-Signature"), { secrets: [process.env.QUBE_WEBHOOK_SECRET] });
300
+ } catch (error) {
301
+ if (error instanceof SignatureError) return res.sendStatus(401);
302
+ throw error;
303
+ }
304
+ // Route by event.type as above, then answer quickly.
305
+ res.sendStatus(200);
306
+ });
307
+ ```
308
+
309
+ ## Development
310
+
311
+ ```sh
312
+ npm test # node:test, no dependencies
313
+ npm run test:types # tsc --strict against test/types.ts (fetches TypeScript with npx)
314
+ node scripts/generate-operations.js # regenerate operations/ from test/fixtures/operations.json
315
+ node scripts/generate-operations.js --check # fail when operations/ is stale
316
+ ```
317
+
318
+ `test/fixtures/test_vectors.json` comes from the QuBe Sync server (`mix webhook_vectors.gen`), and
319
+ `test/fixtures/operations.json` from https://qubesync.com/libraries/operations.json
320
+ (`mix library_operations.gen`). Copy a new version in, then regenerate; don't edit either, or
321
+ anything in `operations/`.
322
+
323
+ ## License
324
+
325
+ MIT