Nordlet

← 博客

如何在 SaaS 平台中集成云端会计 API

构建多租户会计集成的高级实践指南,助您从容应对 OAuth、Webhook、税务规则与对账挑战,同时确保总账万无一失。

Nordlet Team · · 2 分钟阅读

大多数着手在 SaaS 产品中“添加会计功能”的团队,通常都是从将 HTTP 客户端连接到提供商端点并发送一条发票(invoice)开始的。这种做法在演示时没问题,但在以下场景中就会崩溃:客户首次连接两个会计组织(organization)、Webhook 首次重复到达,或者刷新令牌(refresh token)在请求中途轮换并丢失。这种功能的耐用版本绝不是一个简单的客户端,而是一个集成层。

该集成层必须管理租户连接、令牌存储与轮换、特定于提供商的会计规则、记录映射、初始同步与增量同步、具备重试和去重机制的 Webhook 接收、速率限制(rate limit)、版本控制,以及一个能让支持团队在凌晨 2 点看懂的审计追踪(audit trail)日志。本文将详细探讨这种架构以及最关键的决策。

一个实用的心理模型:

SaaS 产品 → 会计集成服务 → 提供商适配器(或统一 API) → 异步同步与对账

会计系统始终是总账对象(科目、税码、已过账交易、分录)的记录系统(system of record)。你的 SaaS 平台则负责维护稳定的跨系统标识符、同步状态、映射元数据以及运营审计日志。

这里所说的“云端会计 API”究竟包含哪些内容

这个短语通常指托管式簿记平台(如 QuickBooks Online、Xero、Sage、Zoho Books 或区域性系统)的 API。它也可以指在单一接口后规范化多个提供商的统一会计 API。

明确这一点非常重要,因为不同的意图对应着不同的实现方式。为了报表读取发票,与根据 SaaS 活动创建发票、发送账单、同步客户与产品、编写日记账分录,或者运行真正的双向同步,并不是同一项工作。在接触任何端点之前,请先确定你要构建的是哪种功能。

对于本文而言,还有一个更重要的区别。支付 API 用于转移资金。银行 API 用于拉取账户数据。开票 API 可以在不暴露总分类账的情况下创建单据。而嵌入式会计总账(embedded accounting ledger)则完全不同:SaaS 公司拥有会计引擎本身。这最后一种方法正是 Nordlet 的定位。Nordlet 没有将你的产品连接到客户的外部账簿,而是通过带有强类型 SDK 和 Webhook 的 API 优先设计,在平台和市场(marketplace)自己的产品内部,为其提供真正的复式记账、不可篡改且随时可供审计的总账、欧盟增值税(EU VAT)处理以及付款功能。如果您是一个正在决定是集成客户的会计系统还是自行运营账簿的市场平台,这就是一个真正的分水岭,我们稍后会回到这个话题。

多租户:为每个连接进行双重作用域限定

在 SaaS 产品中,一个应用程序需要连接到多个客户组织。每个连接必须同时通过 SaaS 租户和提供商的组织标识符进行作用域限定(scope),例如 Xero 租户 ID 或 QuickBooks 区域(realm)ID。

切勿仅通过 OAuth 用户 ID 作为连接的键。一个用户可以授权多个会计组织,而一个组织随着时间推移也可能由不同的用户重新连接。如果这里处理不当,你最终可能会将某个客户的发票发送到另一个客户的账簿中,而这种错误是无法悄无声息地恢复的。

第一个架构决策:直接连接器、统一 API 还是嵌入式 ledger

直接提供商集成

直接集成能让你获得最大的控制权:特定提供商的功能、自定义字段、错误处理、同步频率、数据保留和成本结构。代价是显而易见的。每个提供商都有其自己的 OAuth 流程、数据架构、怪癖、速率限制、Webhook 行为、认证要求和持续维护成本。

统一会计 API

统一 API 在多个会计系统之间提供单一接口,将客户、发票、供应商、科目、付款和产品等实体标准化。这可以缩短初始构建时间。不过,它并没有消除集成的复杂性,如果把它当作万灵药,将会犯下普遍的错误。

在做出承诺之前,请先问清楚:支持哪些提供商和操作?写入功能是否可用,还是只能读取?写入操作是同步的还是排队的?是否可以通过透传(passthrough)获取特定于提供商的字段?数据是否被缓存,时效性如何?谁来处理提供商的宕机和 API 变更?定价是按调用次数、关联账户、记录数还是平台费收取?是否可以导出映射并干净地断开连接?“统一”极少意味着“所有提供商的功能完全对等”。

嵌入式 ledger

如果你的平台的核心问题是为用户运营会计业务,而不是将数据反射到他们现有的账簿中,那么嵌入式总账将改变整个计算方式。拥有成千上万个卖家的市场,或者需要内置审计合规的欧盟增值税和按国家/地区合规要求的 SaaS 产品,通过拥有自己的会计引擎,远比连接 N 个外部连接器更为合适。这就是 Nordlet 的定位:以 API 形式提供真实的账簿、不可篡改的复式记账、带 VIES 验证和 i.SAF 账册生成的按国家/地区增值税、SEPA 导出,以及 XLSX、PDF 或 JSON 格式的报表导出,所有这些都在你的产品内部完成,而无需第三方结账。

我常用的决策规则:

这种情况 最佳匹配
某个会计提供商在你的客户群体中占据主导地位;特定提供商的行为至关重要 直接连接器
广泛的提供商覆盖率和产品上市速度比控制权更重要 统一 API
你是一个需要运营和拥有账簿、且需要欧盟增值税和审计追踪的平台或市场 嵌入式 ledger (Nordlet)
混合现实 混合模式:常见流程使用统一 API,战略性提供商使用直接连接器

推荐的集成架构

将集成与核心业务逻辑隔离

切勿将提供商调用散落到计费、订单、订阅和报表中。应当构建一个专用的集成服务,包含提供商适配器、OAuth 连接管理、规范化模型(canonical model)、ID 映射、同步作业与队列、Webhook 端点、重试和死信(dead-letter)处理、审计日志以及对账工具。这样可以防止 Xero 或 QuickBooks 的字段名称泄露到你的领域模型中。

定义狭窄的规范化模型

仅对你支持的工作流进行建模。典型对象包括:客户、产品或服务项目、发票、发票行、付款、贷项通知单(credit note)、供应商、科目、税率、币种、日记账分录。

保留影响会计的差异。切勿将草稿、已批准和已过账状态扁平化;切勿将含税金额与不含税金额、贷项通知单与负数发票、付款核销与付款存在性,或者面向客户的发票编号与提供商对象 ID 混为一谈。每条记录都应携带 SaaS ID、提供商 ID、提供商名称与 API 版本、真实来源(source-of-truth)指标、状态、时间戳、最后同步时间、外部参考以及错误/对账状态。

维护显式的映射表

将 SaaS 租户映射到会计组织,SaaS 客户映射到提供商联系人,SaaS 产品映射到提供商项目或科目,SaaS 发票映射到提供商发票,SaaS 付款映射到提供商付款,税务类别映射到税码,收入类别映射到总账科目。

切勿将名称或电子邮箱用作永久标识符。名称会变,重复会发生,两个客户也可能共用一个邮箱。应当存储提供商的不可变 ID 并保留原始的 SaaS ID。

添加状态机

为连接赋予明确的状态:未连接、授权挂起、已连接、初始同步挂起、同步中、健康、受速率限制、需要重新授权、提供商不可用、已断开连接、永久失败。为记录赋予其自己的状态:挂起、已发送、已确认、失败可重试、失败需用户操作、已对账、检测到冲突。一个简单的二进制“已连接/未连接”标志无法为支持团队提供任何有用的信息。

OAuth 与连接引导

使用授权码流程(authorization code flow)。客户点击“连接”,你的服务器创建一个短期的授权事务,客户在提供商处进行身份验证并授予权限范围(scope),你将代码交换为令牌、发现授权的组织、针对正确的租户存储连接,并排队等待初始同步。

仔细验证回调:state 必须与服务器端的事务进行对比,验证精确的重定向 URI、提供商签发者以及租户上下文。RFC 9700,即 OAuth 2.0 安全最佳当前实践正式确立了当前的指导方针:在授权码流程中使用 PKCE,避免隐式授权和密码授权,并使用精确的重定向 URI 匹配。

请求最小权限的范围。在不需要写入的地方使用只读权限。范围模型经常变化,因此不要盲目复制旧示例。Xero 的 OAuth 文档指出,宽泛的会计范围正在被细粒度的范围所取代,自 2026 年 3 月起新应用将分配细粒度的范围,而宽泛的范围预计将可用至 2027 年 9 月。

仅在服务器端存储令牌。切勿将其暴露给浏览器 JavaScript、移动客户端、前端日志、分析工具或面向客户的错误消息。实施静态加密、从日志中进行红线脱敏(redact),并保留每个连接的元数据:加密的访问令牌和刷新令牌、过期时间、提供商组织 ID、授予的范围、签发时间、最后成功刷新时间、撤销状态。

刷新令牌轮换是生产环境最容易崩溃的地方

刷新响应可能会返回一个新的刷新令牌。你必须以原子方式持久化最新值,否则会将客户锁在门外。Xero 开发者常见问题解答显示,每次刷新都会产生一个新的刷新令牌,刷新令牌最长有效时间为 60 天,并且如果在响应丢失时,有 30 分钟的宽限期可以使用现有令牌重试。QuickBooks Online 告知开发者应存储来自最近一次响应的最新刷新令牌:刷新令牌最长可达 100 天,并且其值轮换频率最高可达每 24 小时一次。

在实践中:使用基于每个连接的刷新锁、以原子方式替换刷新令牌、通过重试处理瞬时的令牌端点故障、暴露清晰的重新授权状态,并且切勿在访问令牌仍然有效的情况下在每个 API 调用时都进行刷新。

初始同步

作为后台作业运行初始同步。切勿阻塞 OAuth 回调或用户界面。

一个稳健的顺序:获取组织元数据、检索科目、税率、币种、联系人和项目、存储提供商 ID 和映射、仅导入产品所需的历史窗口、按依赖顺序对相关记录进行排队、记录检查点,并且仅在验证成功后将连接标记为健康。

依赖顺序至关重要。发票可能需要先存在联系人、项目、收入科目、税码和币种。切勿盲目创建重复项。应通过存储的外部 ID、客户提供的参考号、定义的匹配规则或针对模糊情况的显式用户选择步骤进行匹配。

增量同步

将 Webhook 视为信号,而非权威记录

Webhook 通常只意味着发生了某些变化。验证签名、记录原始事件、快速返回所需的成功响应、去重、排队等待后续获取、从提供商处检索当前对象、以事务方式应用更新、更新检查点,并将失败路由到重试或对账流程。这样可以处理不完整的有效负载和乱序事件。

提供商规则各不相同,因此不要承诺某种单一的通用行为。Xero Webhooks 在 x-xero-signature 标头中携带 HMAC-SHA256 签名,要求使用 HTTPS,以递减的频率进行长达 24 小时的重试,在连续失败后可能会被禁用,并且可以保存事件长达 31 天以便在恢复后重播。QuickBooks Online 通过 HMAC-SHA256 和验证器令牌进行验证,期望在三秒内返回 HTTP 200,并按照固定计划进行重试,最终演变为 6 小时间隔,Intuit 建议采用异步处理。

去重与幂等性

至少一次(at-least-once)投递是很常见的。应当预料到重复的 Webhook、在写入实际成功但超时后的重试、重排序的事件以及用户重复点击。请使用可用的提供商事件 ID、持久化存储中的唯一事件键、每次写入前生成的的操作 ID、数据库唯一性约束、upsert 语义,以及在模糊超时后的对账查询。

不要假设每个会计 API 都提供原生的幂等键。如果提供商没有提供,请存储内部操作键,并在重试之前检查预期的远程对象是否已经存在。Stripe 的幂等请求以及通用的Webhook 幂等性指南中的模式在这里非常适用。

轮询与变更检测

将 Webhook 与定期轮询结合使用。Webhook 可能会被禁用、延迟、因配置错误而丢失,或者仅限于某些对象。Xero 支持 If-Modified-Since 标头用于查询 UTC 时间戳之后的变更,但其文档警告说,并非所有变更都会更新 UpdatedDateUTC,因此某些部分已付清的交易和联系人字段不会通过该过滤器显示。QuickBooks Online 的变更数据捕获(Change Data Capture)可追踪过去 30 天内的变更,将响应上限设为 1,000 个对象,返回带有 Deleted 状态的已删除实体,并且配合较短的轮询窗口效果最佳。

存储每个提供商、每个租户的检查点,并使用重叠窗口。请求上一次检查点稍早之前的变更,然后按提供商 ID 和修改版本进行去重。这可以防止时钟漂移和部分失败。

会计专用的数据整合

这是将一个能运行的集成与一个值得信赖的集成区分开来的部分。

税务行为并非可以互换的。Xero 的文档指出,当 LineAmountTypes 未设置时,发票、贷项通知单和采购订单默认不含税,收据和银行交易默认含税,而手工日记账默认无税。请使用 TaxType 字段,而不是税率名称。切勿假设“20% 增值税”能够唯一标识一个税码;它可能因国家/地区、交易类型、生效日期和组织配置而异。这就是欧盟多国增值税手动处理成本高昂的地方,也是内置按国家/地区合规要求的嵌入式总账发挥价值的地方。

对于金额,请使用十进制或固定精度算术,切勿使用二进制浮点数。持久化保存币种代码和精度、单价、数量、折扣、税额、净额、总额和汇率。明确决定你是发送行级金额并让提供商计算总额,还是发送预先计算好的总额。将两者混用会产生你的会计师必定会发现的舍入差异。

通过提供商 ID 或代码引用总账科目,而不是显示名称。在写入之前,验证科目、税码、客户、项目和币种处于活动状态且受支持。发票写入失败通常是映射或配置问题,而不是暂时的 API 错误,因此用户界面应解释修复方法,而不是显示“同步失败”。

不要假设对象是等效的。“付款”可能意味着收据、核销、存款、银行交易或日记账分录。SaaS 的“订阅”可能无法干净利落地映射到会计的“发票”。在挑选 API 对象之前,请先定义你想要表示的会计事件。请参阅 Xero 的付款文档,了解一个提供商是如何对此进行建模的。

速率限制与版本控制

限制通常按租户、组织、应用程序或其组合进行作用域限定。根据 Xero 的限制常见问题解答:每个租户每分钟 5 个并发请求、每个租户每分钟 60 个、每个租户每天 5,000 个,整个应用每分钟 10,000 个,同时在 429 响应中带有剩余限制标头和 Retry-After。QuickBooks Online 记录了每个 realm ID 每分钟 500 个请求、每秒 10 个请求,其中批量端点支持每个请求最多 30 个操作。

构建每个租户的速率限制、全局限流、带随机抖动的指数退避重试、对 Retry-After 的尊重、有界并发、分页,以及针对 429 和剩余配额的指标监控。切勿对导入操作使用无限制的并行处理;一个大型客户可能会耗尽该租户上的所有其他作业。

固定或显式管理 API 版本。当未指定时,QuickBooks Online 默认使用子版本 75。将版本集中在配置中,针对沙盒运行契约测试,监控发行说明,并将未知的响应字段视为向前兼容。沙盒无法重现所有生产条件,因此请在上线前测试断开连接、重新连接、撤销同意、令牌过期、速率限制和提供商侧的编辑。

我会首先构建什么

如果我要从明天开始着手,其先后顺序如下:

  1. 定义精确的会计工作流:你要读取或写入哪些事件和对象。
  2. 选择提供商策略:直接连接器、统一 API、嵌入式或混合模式。
  3. 构建带有提供商组织 ID、范围、状态和令牌元数据的租户级连接模型。
  4. 使用授权码流程、PKCE、状态验证、最小权限范围和服务器端令牌存储来实现 OAuth。
  5. 在稳定的内部接口后面构建一个提供商适配器。
  6. 基于不可变的提供商 ID 创建 ID 映射。
  7. 优先实现读取功能;在任何写入之前导入参考数据。
  8. 添加包含分页、速率限制、检查点和依赖顺序的初始同步作业。
  9. 仅在客户、项目、税务和科目映射验证通过后,实现一个狭窄的写入操作(创建发票)。
  10. 通过内部操作 ID、唯一性约束和远程状态检查,使写入操作具备重试安全性。
  11. 添加响应速度快且异步处理的带签名的 Webhook。
  12. 添加轮询和对账,使 Webhook 成为加速器,而不是唯一的真实来源。
  13. 刻意测试故障模式。
  14. 添加支持和审计工具,使故障变得可解释且可修复。
  15. 逐步扩大对象覆盖范围。

FAQ

我应该使用统一 API 还是构建直接连接器?

当某个提供商在你的客户中占据主导地位或者特定提供商的行为至关重要时,请使用直接连接器。当广泛的覆盖范围和速度比控制权更重要时,请考虑统一 API。如果你的真正需求是自己运营账簿而不是同步到客户的系统,这两者都无法回答你的问题,此时应该改用像 Nordlet 这样的嵌入式总账。

Webhook 足以保持同步吗?

不够。Webhook 可能会被禁用、延迟或丢失,并且有效负载可能不完整或乱序。应当将它们视为变更信号,随后获取当前对象,并辅以定期轮询和对账机制。

如何防止刷新令牌故障将客户锁在门外?

在每次刷新时以原子方式持久化最新的刷新令牌,使用基于每个连接的锁,重试临时的令牌端点故障,并暴露清晰的重新授权状态。Xero 的 30 分钟宽限期很有帮助,但原子持久化才是救命稻草。

我可以按客户名称或邮箱匹配记录吗?

不能将其作为永久标识符。名称会变,重复会发生,两个客户也可能共用一个邮箱。应当存储提供商的不可变 ID,并将你自己的 SaaS ID 与之并存。

延伸阅读