connector-quickbooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesequickbooks-client
quickbooks-client
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 :
/, /, /,
//, /, and
(trailing underscore — is a reserved word in Motoko).
Apis/DefaultApi.mosaveCustomergetCustomersaveItemgetItemsaveAccountgetAccountsaveInvoicegetInvoicesendInvoicesavePaymentgetPaymentquery_query针对 QuickBooks Online Accounting API
v3 精选子集的 Motoko 绑定:
Customer、Invoice、Payment、Item、Account。这些 Schema 转录自
Intuit 官方 XSD。全部 12 个操作位于 :
/、/、/、
//、/,以及
(末尾带下划线——因为 是 Motoko 保留字)。
Apis/DefaultApi.mosaveCustomergetCustomersaveItemgetItemsaveAccountgetAccountsaveInvoicegetInvoicesendInvoicesavePaymentgetPaymentquery_queryTrigger phrases
触发短语
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.
任何提及以下内容的请求都应使用此 skill: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"。
不在此切片范围内: Estimate、Bill、Vendor、PurchaseOrder、CreditMemo 以及
其余 QBO 实体集——该规范经过人工精选,仅涵盖 Customer、Invoice、
Payment、Item 和 Account。如果请求需要其中其他实体,应明确说明,而不是将其
映射为 Invoice;必须先根据 Intuit 的 XSD 重新生成该规范。
How QuickBooks authentication works (read before wiring)
QuickBooks 身份验证的工作方式(接入前必读)
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
result and re-authenticate off-chain.
#Err("auth_expired")Two identifiers travel with every call:
- — the QuickBooks company id, obtained during the OAuth handshake. It is the first argument to every operation.
realmId - — the API minor version (default
minorversion); pass"75"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):
- on the exchange too — the token response is non-deterministic, so a replicated exchange duplicates across ~13 replicas and fails IC consensus.
is_replicated = ?false - The Client Secret leaks with exported source — Caffeine can export the app
to a or public GitHub repo, carrying the secret along. Scope it minimally and rotate if the source is shared.
.zip
Persist the per-user refresh/session token across upgrades (stable memory) so a
redeploy doesn't force re-authentication. OAuth scope: .
com.intuit.quickbooks.accountingQBO 使用 OAuth 2.0 Authorization Code——不存在静态 API key。每位最终用户
都会授权其 QuickBooks company;应用将 authorization code 换取短时有效的
Bearer access token(约 1 小时),并在调用时传给 client。令牌过期后,
API 返回 HTTP 401——应返回 结果,并在 off-chain
重新进行身份验证。
#Err("auth_expired")每次调用都会携带两个标识符:
- ——QuickBooks 的 company id,在 OAuth handshake 期间取得。 它是每个操作的第一个参数。
realmId - ——API 次要版本(默认为
minorversion);传入"75"可省略该参数, 并使用账号的默认版本。""
令牌交换本身也是对 Intuit token endpoint 的 on-chain outcall,并且需要
应用的 Client Secret。这里有两个风险点(与 googlemail connector 相同):
- 令牌交换也必须设置 ——令牌响应具有非确定性, 如果使用 replicated 交换,请求会在约 13 个 replica 上重复执行,并导致 IC consensus 失败。
is_replicated = ?false - 导出源代码时 Client Secret 也会泄露——Caffeine 可以将应用导出为 或公开 GitHub 仓库,从而一并携带该 secret。应尽量缩小其权限范围;如果 源代码被共享,则应轮换 secret。
.zip
在升级过程中(使用 stable memory)持久保存每位用户的 refresh/session token,
避免重新部署时被迫重新认证。OAuth scope:。
com.intuit.quickbooks.accountingCreate vs. update vs. delete (important QBO semantics)
创建、更新与删除(重要的 QBO 语义)
- Create and update BOTH use the (POST) operation. It's an update when the body carries the entity's
save<Entity>and currentId(fetch them first viaSyncToken); setget<Entity>for a partial update. Withoutsparse = ?true/Idit's a create.SyncToken - Delete (transactions only — ,
Invoice): callPayment/saveInvoicewithsavePaymentand a body carryingoperation = ?#delete+Id. TheSyncTokenargument is optional (operation), not a bare variant. Pass?SaveInvoiceOperationParameterfor 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". Onlynullis routinely needed. Name-list entities (?#delete,Customer,Item) are not deletable — setAccountto deactivate instead (theirActive = ?falseops take nosave*argument).operation - Responses are wrapped: returns
getCustomerwith aCustomerResponsefield (and aCustomer); same shape per entity.timereturns aquery_whoseQueryResponsefield holds arrays per entity type. The operation isQueryResponse, notquery_.query - Invoice lines and Payment lines are different types. is
Invoice.Line, where[Line]is required.DetailTypeisPayment.Line—[PaymentLine]with no{Amount, LinkedTxn, …}, 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.DetailType
- **创建和更新都使用 (POST)操作。**如果请求体同时携带实体的
save<Entity>和当前Id(应先通过SyncToken获取),则视为更新; 部分更新需要设置get<Entity>。如果缺少sparse = ?true/Id,则执行创建。SyncToken - 删除(仅限交易类型——、
Invoice):调用Payment或saveInvoice,传入savePayment,并在请求体中携带operation = ?#delete+Id。SyncToken参数是可选的 (operation),而不是裸 variant。正常创建或更新时 传入?SaveInvoiceOperationParameter——client 随后会完全省略 query parameter,这正是 QBO 所要求的;规范写道:"Omit for create/update; 'delete' removes the invoice"。 通常只有null是必需的。名称列表实体 (?#delete、Customer、Item)不可删除——应改用Account将其停用(它们的Active = ?false操作不接受save*参数)。operation - 返回结果采用包装结构:返回
getCustomer,其中包含CustomerResponse字段(以及Customer);每种实体均采用相同结构。time返回query_,其QueryResponse字段按实体类型保存数组。该操作名为QueryResponse,而不是query_。query - Invoice 行与 Payment 行是不同类型。是
Invoice.Line,其中[Line]为必填。DetailType是Payment.Line——即[PaymentLine],不包含{Amount, LinkedTxn, …};当付款被核销到发票时, Intuit 实际发送的正是这种结构。在 0.2.0 之前,二者共用同一种类型,这导致 每个付款响应都无法解码。DetailType
Frontend surfaces (two pages)
前端界面(两个页面)
Like any per-user OAuth connector, an app needs two surfaces:
- An admin-gated Intuit app configuration page — the Client ID and Client Secret are canister-wide, set once by an admin; never expose the Client Secret to ordinary users.
- A per-user OAuth 2.0 handshake page — each user connects their own
QuickBooks company (yielding their + token); re-prompt on HTTP 401.
realmId
与任何按用户进行 OAuth 的 connector 一样,应用需要两个界面:
- 仅管理员可访问的 Intuit 应用配置页面——Client ID 和 Client Secret 属于整个 canister,由管理员设置一次;绝不能向普通用户 暴露 Client Secret。
- 每位用户各自的 OAuth 2.0 handshake 页面——每位用户连接自己的
QuickBooks company(获取其 和 token);收到 HTTP 401 时重新 提示用户授权。
realmId
Usage
用法
mo
import { saveInvoice; getInvoice; sendInvoice; saveCustomer }
"mo:quickbooks-client/Apis/DefaultApi";
import Invoice "mo:quickbooks-client/Models/Invoice";
import { defaultConfig } "mo:quickbooks-client/Config";
let cfg = {
defaultConfig with
auth = ?#bearer "<off-chain OAuth2 access token>";
is_replicated = ?false; // non-replicated: required for writes; reads too
};
let realmId = "<company id from OAuth handshake>";
// Build an invoice: one sales line, $100, item "1", billed to customer "58".
// Invoice has no required fields → init {} then record-update the optionals.
// NOTE: DetailType is REQUIRED on a Line, and its variants are lowercase.
let inv = {
Invoice.init {} with
CustomerRef = ?{ value = "58"; name = null };
Line = ?[ {
DetailType = #salesitemlinedetail; // required; lowercase variant
Amount = ?100.0;
Description = ?"Consulting";
SalesItemLineDetail = ?{ ItemRef = ?{ value = "1"; name = null };
Qty = ?1.0; UnitPrice = ?100.0; TaxCodeRef = null; ServiceDate = null };
Id = null; LineNum = null; LinkedTxn = null;
} ];
};
// saveInvoice(config, realmId, invoice, minorversion, operation)
// `operation` is optional: null omits it — the create/update path.
let created = await* saveInvoice(cfg, realmId, inv, "75", null);
// Email it to the customer (config, realmId, invoiceId, sendTo, minorversion):
// ignore await* sendInvoice(cfg, realmId, "<invoiceId>", "cust@example.com", "75");mo
import { saveInvoice; getInvoice; sendInvoice; saveCustomer }
"mo:quickbooks-client/Apis/DefaultApi";
import Invoice "mo:quickbooks-client/Models/Invoice";
import { defaultConfig } "mo:quickbooks-client/Config";
let cfg = {
defaultConfig with
auth = ?#bearer "<off-chain OAuth2 access token>";
is_replicated = ?false; // non-replicated: required for writes; reads too
};
let realmId = "<company id from OAuth handshake>";
// Build an invoice: one sales line, $100, item "1", billed to customer "58".
// Invoice has no required fields → init {} then record-update the optionals.
// NOTE: DetailType is REQUIRED on a Line, and its variants are lowercase.
let inv = {
Invoice.init {} with
CustomerRef = ?{ value = "58"; name = null };
Line = ?[ {
DetailType = #salesitemlinedetail; // required; lowercase variant
Amount = ?100.0;
Description = ?"Consulting";
SalesItemLineDetail = ?{ ItemRef = ?{ value = "1"; name = null };
Qty = ?1.0; UnitPrice = ?100.0; TaxCodeRef = null; ServiceDate = null };
Id = null; LineNum = null; LinkedTxn = null;
} ];
};
// saveInvoice(config, realmId, invoice, minorversion, operation)
// `operation` is optional: null omits it — the create/update path.
let created = await* saveInvoice(cfg, realmId, inv, "75", null);
// Email it to the customer (config, realmId, invoiceId, sendTo, minorversion):
// ignore await* sendInvoice(cfg, realmId, "<invoiceId>", "cust@example.com", "75");Notes
注意事项
- Use for writes (
is_replicated = ?false,save*, delete viasendInvoice). 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 (operation = ?#delete,get*) also usequery_(one node, ~13× cheaper).?false - Update needs a fresh — always
SyncTokenfirst, copy itsget<Entity>into the body, thenSyncToken. A stale token → HTTP 400 ("stale object").save<Entity> - defaults to
minorversion; pass"75"to omit."" - QBO returns errors as an whose list field is
ErrorResponse— note the trailing underscore,Fault.Error_being reserved — so surfaceErrorto the caller; never blind-retry a write.Fault.Error_[0].Message - Sandbox vs production: point the client host at
for the Intuit sandbox company during development (edit the server in
sandbox-quickbooks.api.intuit.com), andConfigfor production.quickbooks.api.intuit.com
- 写入操作(、
save*,以及通过sendInvoice删除) 必须使用operation = ?#delete。这些 outcall 不是幂等的,而且 QBO 的 响应具有非确定性;在 replicated 模式下,每个 replica 都会发出请求, 从而创建重复的发票或付款,并导致 IC consensus 失败。读取操作(is_replicated = ?false、get*)也使用query_(由单个节点执行,成本约低 13 倍)。?false - 更新需要最新的 ——始终先调用
SyncToken,将其get<Entity>复制到请求体,然后调用SyncToken。token 过期会返回 HTTP 400("stale object")。save<Entity> - 默认为
minorversion;传入"75"可省略。"" - QBO 以 形式返回错误,其列表字段为
ErrorResponse——注意 末尾的下划线,因为Fault.Error_是保留字——因此应向调用方返回Error;切勿盲目重试写入操作。Fault.Error_[0].Message - 沙箱与生产环境:开发期间,将 client host 指向
,以连接 Intuit sandbox company; 生产环境则指向
sandbox-quickbooks.api.intuit.com。quickbooks.api.intuit.com