npx skills add ...
npx skills add caffeinelabs/skills --skill connector-quickbooks
Use the `quickbooks-client` mops package whenever the user asks the canister to create/read/update invoices, customers, payments, items, or chart-of-account entries in QuickBooks Online (QBO), email an invoice, or run a QuickBooks query. The package wraps the QuickBooks Online Accounting API v3 at `https://quickbooks.api.intuit.com` via outbound HTTPS calls.
npx skills add caffeinelabs/skills --skill connector-quickbooks
Motoko bindings for a curated slice of the QuickBooks Online Accounting API
v3:
Customer, Invoice, Payment, Item, Account. Schemas were transcribed from
Intuit's official XSD. All 12 operations live in Apis/DefaultApi.mo:
saveCustomer/getCustomer, saveItem/getItem, saveAccount/getAccount,
saveInvoice/getInvoice/sendInvoice, savePayment/getPayment, and query_
(trailing underscore — query is a reserved word in Motoko).
Reach for this skill on any request mentioning: QuickBooks, QBO, invoice, "bill a customer", "send an invoice", record/receive a payment, customer, accounting, bookkeeping, chart of accounts, item/product/service, "sync to QuickBooks".
Not in this slice: Estimate, Bill, Vendor, PurchaseOrder, CreditMemo and the rest of the QBO entity set — the spec is hand-curated to Customer, Invoice, Payment, Item and Account. If a request needs one of the others, say so rather than mapping it onto Invoice; the spec has to be re-spun from Intuit's XSD first.
QBO uses OAuth 2.0 Authorization Code — there is no static API key. Each
end-user authorises their QuickBooks company; the app exchanges the authorization
code for a short-lived Bearer access token (~1 hour) and passes it to the
client at call time. On expiry the API returns HTTP 401 — surface a
#Err("auth_expired") result and re-authenticate off-chain.
Two identifiers travel with every call:
realmId — the QuickBooks company id, obtained during the OAuth handshake.
It is the first argument to every operation.minorversion — the API minor version (default "75"); pass "" to omit
and use the account default.The token exchange is itself an on-chain outcall to Intuit's token endpoint and needs the app's Client Secret. Two hazards (identical to the googlemail connector):
is_replicated = ?false on the exchange too — the token response is
non-deterministic, so a replicated exchange duplicates across ~13 replicas and
fails IC consensus..zip or public GitHub repo, carrying the secret along. Scope it minimally
and rotate if the source is shared.Persist the per-user refresh/session token across upgrades (stable memory) so a
redeploy doesn't force re-authentication. OAuth scope: com.intuit.quickbooks.accounting.
save<Entity> (POST) operation. It's an
update when the body carries the entity's Id and current SyncToken
(fetch them first via get<Entity>); set sparse = ?true for a partial update.
Without Id/SyncToken it's a create.Invoice, Payment): call saveInvoice /
savePayment with operation = ?#delete and a body carrying Id + SyncToken.
The operation argument is optional (?SaveInvoiceOperationParameter),
not a bare variant. Pass null for a normal create or update — the client
then omits the query parameter entirely, which is what QBO wants; the spec
says "Omit for create/update; 'delete' removes the invoice". Only ?#delete
is routinely needed. Name-list entities
(Customer, Item, Account) are not deletable — set Active = ?false
to deactivate instead (their save* ops take no operation argument).getCustomer returns CustomerResponse with a
Customer field (and a time); same shape per entity. query_ returns a
QueryResponse whose QueryResponse field holds arrays per entity type.
The operation is query_, not query.Invoice.Line is
[Line], where DetailType is required. Payment.Line is
[PaymentLine] — {Amount, LinkedTxn, …} with no DetailType, which is
what Intuit actually sends when a payment is applied to invoices. They were
one shared type until 0.2.0, and that made every payment response fail to
decode.Like any per-user OAuth connector, an app needs two surfaces:
realmId + token); re-prompt on HTTP 401.is_replicated = ?false for writes (save*, sendInvoice, delete via
operation = ?#delete). These outcalls are non-idempotent and QBO's response is
non-deterministic; in replicated mode every replica issues the request — creating
duplicate invoices/payments and failing IC consensus. Reads (get*, query_)
also use ?false (one node, ~13× cheaper).SyncToken — always get<Entity> first, copy its
SyncToken into the body, then save<Entity>. A stale token → HTTP 400
("stale object").minorversion defaults to "75"; pass "" to omit.ErrorResponse whose list field is Fault.Error_ —
note the trailing underscore, Error being reserved — so surface
Fault.Error_[0].Message to the caller; never blind-retry a write.sandbox-quickbooks.api.intuit.com for the Intuit sandbox company during
development (edit the server in Config), and quickbooks.api.intuit.com for
production.*