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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +324 -2
- package/client.js +660 -0
- package/index.d.ts +367 -0
- package/index.js +409 -0
- package/index.mjs +22 -0
- package/operations/account.d.ts +263 -0
- package/operations/account.js +50 -0
- package/operations/account_tax_line_info.d.ts +31 -0
- package/operations/account_tax_line_info.js +26 -0
- package/operations/aging_report.d.ts +100 -0
- package/operations/aging_report.js +26 -0
- package/operations/alert.d.ts +118 -0
- package/operations/alert.js +50 -0
- package/operations/ar_refund_credit_card.d.ts +201 -0
- package/operations/ar_refund_credit_card.js +38 -0
- package/operations/bar_code.d.ts +40 -0
- package/operations/bar_code.js +26 -0
- package/operations/bill.d.ts +612 -0
- package/operations/bill.js +50 -0
- package/operations/bill_payment_check.d.ts +276 -0
- package/operations/bill_payment_check.js +50 -0
- package/operations/bill_payment_credit_card.d.ts +178 -0
- package/operations/bill_payment_credit_card.js +38 -0
- package/operations/bill_to_pay.d.ts +54 -0
- package/operations/bill_to_pay.js +26 -0
- package/operations/billing_rate.d.ts +159 -0
- package/operations/billing_rate.js +38 -0
- package/operations/budget_summary_report.d.ts +71 -0
- package/operations/budget_summary_report.js +26 -0
- package/operations/build_assembly.d.ts +264 -0
- package/operations/build_assembly.js +50 -0
- package/operations/charge.d.ts +330 -0
- package/operations/charge.js +50 -0
- package/operations/check.d.ts +394 -0
- package/operations/check.js +50 -0
- package/operations/class.d.ts +170 -0
- package/operations/class.js +50 -0
- package/operations/cleared_status.d.ts +43 -0
- package/operations/cleared_status.js +26 -0
- package/operations/client.d.ts +254 -0
- package/operations/company.d.ts +36 -0
- package/operations/company.js +26 -0
- package/operations/company_activity.d.ts +32 -0
- package/operations/company_activity.js +26 -0
- package/operations/credit_card_charge.d.ts +280 -0
- package/operations/credit_card_charge.js +50 -0
- package/operations/credit_card_credit.d.ts +280 -0
- package/operations/credit_card_credit.js +50 -0
- package/operations/credit_memo.d.ts +642 -0
- package/operations/credit_memo.js +50 -0
- package/operations/currency.d.ts +195 -0
- package/operations/currency.js +50 -0
- package/operations/custom_detail_report.d.ts +108 -0
- package/operations/custom_detail_report.js +26 -0
- package/operations/custom_summary_report.d.ts +119 -0
- package/operations/custom_summary_report.js +26 -0
- package/operations/customer.d.ts +639 -0
- package/operations/customer.js +50 -0
- package/operations/customer_msg.d.ts +121 -0
- package/operations/customer_msg.js +38 -0
- package/operations/customer_type.d.ts +127 -0
- package/operations/customer_type.js +38 -0
- package/operations/data_event_recovery_info.d.ts +61 -0
- package/operations/data_event_recovery_info.js +38 -0
- package/operations/data_ext.d.ts +197 -0
- package/operations/data_ext.js +50 -0
- package/operations/data_ext_def.d.ts +173 -0
- package/operations/data_ext_def.js +62 -0
- package/operations/date_driven_terms.d.ts +141 -0
- package/operations/date_driven_terms.js +38 -0
- package/operations/deposit.d.ts +402 -0
- package/operations/deposit.js +50 -0
- package/operations/employee.d.ts +683 -0
- package/operations/employee.js +50 -0
- package/operations/employee_default.d.ts +133 -0
- package/operations/employee_default.js +38 -0
- package/operations/entity.d.ts +96 -0
- package/operations/entity.js +26 -0
- package/operations/estimate.d.ts +590 -0
- package/operations/estimate.js +50 -0
- package/operations/form1099_category_account_mapping.d.ts +89 -0
- package/operations/form1099_category_account_mapping.js +38 -0
- package/operations/general_detail_report.d.ts +108 -0
- package/operations/general_detail_report.js +26 -0
- package/operations/general_summary_report.d.ts +115 -0
- package/operations/general_summary_report.js +26 -0
- package/operations/host.d.ts +43 -0
- package/operations/host.js +26 -0
- package/operations/index.js +397 -0
- package/operations/inventory_adjustment.d.ts +389 -0
- package/operations/inventory_adjustment.js +50 -0
- package/operations/inventory_site.d.ts +247 -0
- package/operations/inventory_site.js +50 -0
- package/operations/invoice.d.ts +682 -0
- package/operations/invoice.js +50 -0
- package/operations/item.d.ts +100 -0
- package/operations/item.js +26 -0
- package/operations/item_assemblies_can_build.d.ts +41 -0
- package/operations/item_assemblies_can_build.js +26 -0
- package/operations/item_discount.d.ts +256 -0
- package/operations/item_discount.js +50 -0
- package/operations/item_fixed_asset.d.ts +382 -0
- package/operations/item_fixed_asset.js +50 -0
- package/operations/item_group.d.ts +236 -0
- package/operations/item_group.js +50 -0
- package/operations/item_inventory.d.ts +350 -0
- package/operations/item_inventory.js +50 -0
- package/operations/item_inventory_assembly.d.ts +368 -0
- package/operations/item_inventory_assembly.js +50 -0
- package/operations/item_non_inventory.d.ts +264 -0
- package/operations/item_non_inventory.js +50 -0
- package/operations/item_other_charge.d.ts +244 -0
- package/operations/item_other_charge.js +50 -0
- package/operations/item_payment.d.ts +220 -0
- package/operations/item_payment.js +50 -0
- package/operations/item_receipt.d.ts +362 -0
- package/operations/item_receipt.js +50 -0
- package/operations/item_sales_tax.d.ts +228 -0
- package/operations/item_sales_tax.js +50 -0
- package/operations/item_sales_tax_group.d.ts +192 -0
- package/operations/item_sales_tax_group.js +50 -0
- package/operations/item_service.d.ts +256 -0
- package/operations/item_service.js +50 -0
- package/operations/item_sites.d.ts +81 -0
- package/operations/item_sites.js +26 -0
- package/operations/item_subtotal.d.ts +188 -0
- package/operations/item_subtotal.js +50 -0
- package/operations/job_report.d.ts +99 -0
- package/operations/job_report.js +26 -0
- package/operations/job_type.d.ts +127 -0
- package/operations/job_type.js +38 -0
- package/operations/journal_entry.d.ts +339 -0
- package/operations/journal_entry.js +50 -0
- package/operations/lead.d.ts +291 -0
- package/operations/lead.js +50 -0
- package/operations/list.d.ts +39 -0
- package/operations/list.js +26 -0
- package/operations/list_deleted.d.ts +44 -0
- package/operations/list_deleted.js +26 -0
- package/operations/list_display.d.ts +65 -0
- package/operations/list_display.js +38 -0
- package/operations/list_merge.d.ts +58 -0
- package/operations/list_merge.js +26 -0
- package/operations/other_name.d.ts +280 -0
- package/operations/other_name.js +50 -0
- package/operations/paycheck.d.ts +82 -0
- package/operations/paycheck.js +26 -0
- package/operations/payment_method.d.ts +131 -0
- package/operations/payment_method.js +38 -0
- package/operations/payroll_detail_report.d.ts +97 -0
- package/operations/payroll_detail_report.js +26 -0
- package/operations/payroll_item.d.ts +83 -0
- package/operations/payroll_item.js +26 -0
- package/operations/payroll_item_addition.d.ts +95 -0
- package/operations/payroll_item_addition.js +38 -0
- package/operations/payroll_item_company_contribution.d.ts +105 -0
- package/operations/payroll_item_company_contribution.js +38 -0
- package/operations/payroll_item_deduction.d.ts +101 -0
- package/operations/payroll_item_deduction.js +38 -0
- package/operations/payroll_item_direct_deposit.d.ts +65 -0
- package/operations/payroll_item_direct_deposit.js +38 -0
- package/operations/payroll_item_non_wage.d.ts +92 -0
- package/operations/payroll_item_non_wage.js +26 -0
- package/operations/payroll_item_tax.d.ts +108 -0
- package/operations/payroll_item_tax.js +38 -0
- package/operations/payroll_item_wage.d.ts +169 -0
- package/operations/payroll_item_wage.js +50 -0
- package/operations/payroll_item_workers_compensation.d.ts +77 -0
- package/operations/payroll_item_workers_compensation.js +38 -0
- package/operations/payroll_last_period.d.ts +31 -0
- package/operations/payroll_last_period.js +26 -0
- package/operations/payroll_liability_adjustment.d.ts +137 -0
- package/operations/payroll_liability_adjustment.js +38 -0
- package/operations/payroll_liability_check.d.ts +82 -0
- package/operations/payroll_liability_check.js +26 -0
- package/operations/payroll_prior_payment.d.ts +126 -0
- package/operations/payroll_prior_payment.js +38 -0
- package/operations/payroll_summary_report.d.ts +107 -0
- package/operations/payroll_summary_report.js +26 -0
- package/operations/payroll_year_to_date_adjustment.d.ts +156 -0
- package/operations/payroll_year_to_date_adjustment.js +38 -0
- package/operations/preferences.d.ts +64 -0
- package/operations/preferences.js +38 -0
- package/operations/price_level.d.ts +234 -0
- package/operations/price_level.js +50 -0
- package/operations/purchase_order.d.ts +620 -0
- package/operations/purchase_order.js +50 -0
- package/operations/qb_command_exec.d.ts +49 -0
- package/operations/qb_command_exec.js +26 -0
- package/operations/receive_payment.d.ts +450 -0
- package/operations/receive_payment.js +50 -0
- package/operations/receive_payment_to_deposit.d.ts +36 -0
- package/operations/receive_payment_to_deposit.js +26 -0
- package/operations/sales_order.d.ts +612 -0
- package/operations/sales_order.js +50 -0
- package/operations/sales_receipt.d.ts +650 -0
- package/operations/sales_receipt.js +50 -0
- package/operations/sales_rep.d.ts +170 -0
- package/operations/sales_rep.js +50 -0
- package/operations/sales_tax_code.d.ts +194 -0
- package/operations/sales_tax_code.js +50 -0
- package/operations/sales_tax_payable.d.ts +42 -0
- package/operations/sales_tax_payable.js +26 -0
- package/operations/sales_tax_payment_check.d.ts +254 -0
- package/operations/sales_tax_payment_check.js +50 -0
- package/operations/sales_tax_return.d.ts +35 -0
- package/operations/sales_tax_return.js +26 -0
- package/operations/sales_tax_return_line.d.ts +36 -0
- package/operations/sales_tax_return_line.js +26 -0
- package/operations/ship_method.d.ts +121 -0
- package/operations/ship_method.js +38 -0
- package/operations/special_account.d.ts +40 -0
- package/operations/special_account.js +26 -0
- package/operations/special_item.d.ts +44 -0
- package/operations/special_item.js +26 -0
- package/operations/standard_terms.d.ts +133 -0
- package/operations/standard_terms.js +38 -0
- package/operations/template.d.ts +36 -0
- package/operations/template.js +26 -0
- package/operations/terms.d.ts +90 -0
- package/operations/terms.js +26 -0
- package/operations/time_report.d.ts +84 -0
- package/operations/time_report.js +26 -0
- package/operations/time_tracking.d.ts +214 -0
- package/operations/time_tracking.js +50 -0
- package/operations/to_do.d.ts +254 -0
- package/operations/to_do.js +50 -0
- package/operations/transaction.d.ts +244 -0
- package/operations/transaction.js +26 -0
- package/operations/transfer.d.ts +178 -0
- package/operations/transfer.js +50 -0
- package/operations/transfer_inventory.d.ts +290 -0
- package/operations/transfer_inventory.js +50 -0
- package/operations/txn.d.ts +39 -0
- package/operations/txn.js +26 -0
- package/operations/txn_deleted.d.ts +44 -0
- package/operations/txn_deleted.js +26 -0
- package/operations/txn_display.d.ts +69 -0
- package/operations/txn_display.js +38 -0
- package/operations/txn_void.d.ts +39 -0
- package/operations/txn_void.js +26 -0
- package/operations/types.d.ts +1259 -0
- package/operations/unit_of_measure_set.d.ts +168 -0
- package/operations/unit_of_measure_set.js +38 -0
- package/operations/vehicle.d.ts +168 -0
- package/operations/vehicle.js +50 -0
- package/operations/vehicle_mileage.d.ts +147 -0
- package/operations/vehicle_mileage.js +38 -0
- package/operations/vendor.d.ts +541 -0
- package/operations/vendor.js +50 -0
- package/operations/vendor_credit.d.ts +365 -0
- package/operations/vendor_credit.js +50 -0
- package/operations/vendor_type.d.ts +127 -0
- package/operations/vendor_type.js +38 -0
- package/operations/workers_comp_code.d.ts +199 -0
- package/operations/workers_comp_code.js +50 -0
- 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
|
-
#
|
|
1
|
+
# qubesync (Node)
|
|
2
2
|
|
|
3
|
-
|
|
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
|