Skip to content
手册 v0.1.0 · KaiderBooks v2.0.0 · 2026-07-22

开发者 API

一句话说清

KaiderBooks API 是 JSON over HTTPS 接口。当前版本统一使用 /api/v1,受保护请求使用 JWT Bearer token;本文还记录 API Key、Webhook 和清算中心的实际可用边界。

以当前实现为准

管理后台可以创建 API Key 并设置 scope,但当前 API 请求守卫只接受 Bearer JWT,尚未读取 X-Api-Key 或其他 API-key header。第 4 节的通用业务 Webhook 当前也没有签名 header;第 9 节的 POS Webhook 另用一次性返回的共享 secret 认证。在服务端正式支持前,不要把 API Key 用作请求凭证,也不要把未签名的通用 Webhook 当作可信指令。

1. 基础约定

  • 基础 URL:https://books.chuhaisg.com/api/v1
  • 内容类型:请求与响应默认使用 application/json
  • 日期:YYYY-MM-DD;时间戳为带时区的 ISO 8601
  • 金额:API JSON 中所有以 Cents 结尾的金额都是整数分,例如 S$12.34 写成 1234
  • ID:资源 ID 使用 UUID
  • 版本:主版本放在路径中。当前为 /v1;不兼容变更进入新的主版本路径,v1 内只做向后兼容的字段新增或修正

分页与排序

采用通用分页的列表端点接受:

参数约定
page正整数,默认 1
pageSize正整数,默认 20,最大 100
sort字符串;具体可排序字段由资源决定,常见降序写法为 -fieldfield:desc
q支持该参数的列表端点用于文本搜索

典型分页响应为 { "items": [...], "total": 42 }。清算中心的列表端点目前返回数组,并使用各自的筛选参数,不套用通用分页。

2. 认证与会话

登录换取 JWT

bash
curl -X POST 'https://books.chuhaisg.com/api/v1/auth/login' \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{"email":"demo@firm.sg","password":"YOUR_PASSWORD"}'

成功响应包含 accessTokenuserfirmroleclientIdfirms。把 access token 放进后续请求:

bash
curl 'https://books.chuhaisg.com/api/v1/clients?page=1&pageSize=20' \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Refresh token 轮换

登录同时通过 HttpOnly cookie refresh_token 写入 refresh token;cookie 路径为 /api/v1/auth,有效期 7 天。调用 POST /auth/refresh 时浏览器或 cookie jar 必须带上该 cookie。服务端会在同一事务中撤销旧 token、签发新 token,并重写 cookie;旧 token 不能再次使用。POST /auth/logout 会撤销当前 refresh token 并清除 cookie。

bash
curl -X POST 'https://books.chuhaisg.com/api/v1/auth/refresh' \
  -b cookies.txt -c cookies.txt

API Key 与 scope

合作伙伴或经理可通过 /api/v1/api-keys 列出、创建和吊销 API Key,且需要 api-keys.manage 权限。完整 key 以 sk_live_ 开头,只在创建响应中出现一次;数据库只保存 hash 与前缀。

scope设计用途
read读取资源与报表
write新建、更新、过账或匹配业务资源
filing提交申报

当前版本只持久化这些 scope,尚未把 API Key 接入认证守卫,也没有可用的 API-key header。所有业务请求仍须使用 Bearer JWT。

3. 权限模型

事务所有四个内部角色:partnermanageraccountantintern。另有面向门户的 client 身份,其访问范围固定到 membership 的 clientId;它不是事务所内部授权层级。

权限矩阵包含 26 个 permission key。请求先经过角色门禁,再按当前 firm、角色和 permission key 查矩阵;矩阵可以收紧角色权限,不能绕过角色门禁。常见例子:

场景permission key说明
过账 / 冲销凭证journals.post / journals.reverse还要求角色为 partner、manager 或 accountant
导出 / 查看报表reports.export / reports.view查看与导出分开授权
提交申报filings.submit与 API Key 的 filing scope 是不同层次的概念
查看 / 管理清算settlements.view / settlements.manage所有清算读取与写入端点分别使用这两个 key
管理 Webhookwebhooks.manage管理端点还要求 partner 或 manager

其余 key 覆盖账簿编辑、复核、自动化、账单审批、工资、客户、用户、角色、API Key、审计、发票、付款、期间锁、机构设置、合规和公司秘书等功能。缺少所需 key 返回 403 FORBIDDEN,message 会指出 key,例如 Permission denied: settlements.manage

4. Webhook

管理端点与事件

GET/POST /webhooksPATCH/DELETE /webhooks/{id}POST /webhooks/{id}/test 需要 partner 或 manager 角色以及 webhooks.manage

创建时提交 url、至少一个 events 字符串和可选的 statusactivedisabled)。当前事件订阅值是开放字符串,不是固定 enum;管理界面的常见业务值为 invoice.sent。代码中唯一固定并实际投递的测试事件为 webhook.test

bash
curl -X POST 'https://books.chuhaisg.com/api/v1/webhooks' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/hooks/kaiderbooks","events":["invoice.sent"],"status":"active"}'

curl -X POST 'https://books.chuhaisg.com/api/v1/webhooks/WEBHOOK_UUID/test' \
  -H "Authorization: Bearer $ACCESS_TOKEN"

测试投递向目标 URL 发送 JSON:idevent: "webhook.test"createdAtfirmId,header 为 Content-Type: application/jsonx-kaiderbooks-event: webhook.test,超时 5 秒。API 返回 deliveredstatusstatusCode

签名校验

当前实现不生成 secret,也不发送签名或时间戳 header,因此没有可执行的签名校验流程。接收端现阶段只能把 x-kaiderbooks-event 用于路由,不能用它证明来源;应使用专用 HTTPS URL、网络层访问控制与幂等处理,并等待服务端增加签名协议后再信任业务投递。

5. 端点总表

下表路径均相对于 https://books.chuhaisg.com/api/v1

模块基础路径主要操作
auth/authlogin、refresh、logout、me、switch-firm
clients/clients客户列表、新建、更新、删除
journals/journals列表/详情、草稿 CRUD、post、reverse
ar/quotations/invoices/receipts报价发送/接受/转发票,发票发送/作废,收款登记/作废
ap/bills/payments账单 CRUD/审批/作废,付款登记/作废
bank/bank-accounts/bank-transactions银行账户 CRUD,CSV 导入、匹配、取消匹配、排除
settlements/outlets/daily-closes/platform-settlements/settlement-rules门店、日结、平台结算导入/过账/匹配/冲销、费率规则
supplier-statements/supplier-statements供应商对账单 CRUD、CSV 导入、自动/手工匹配、报告、reconcile
recipes/stocktakes/recipes/stocktakes配方 CRUD、理论毛利、盘点快照/实点/完成
pos/pos-connections/pos/webhooksPOS 连接 CRUD、收据 Webhook、按日同步班结单
reports/reportstrial balance、general ledger、P&L、balance sheet、cashflow、GST F5、daily profit 与导出
filing/filing-deadlines/filing-records/reminder-rules截止日、提交记录、提醒规则
payroll/payroll-runspayroll run 与 payslip CRUD、complete
compliance/complianceoverview、risk assessments、beneficial owners、CDD、monitoring reviews、STR reports

6. 清算中心端点详表

所有清算读取需要 settlements.view,写入需要 settlements.manage。列表可省略 clientId 以查询当前 firm;client 身份始终被限制到自己的 clientId。下列所有 JSON 金额均为整数分

Outlets

方法路径关键参数响应字段
GET/outletsquery: clientId?数组;id, clientId, code, name, address, active 及审计字段
GET/outlets/{id}path: outlet UUID单个 outlet
POST/outletsbody: clientId, code, name, address?, active?新建 outlet;同一客户内 code 唯一
PATCH/outlets/{id}body: code?, name?, address?, active?,至少一项更新后的 outlet
DELETE/outlets/{id}path: outlet UUID{ "id": "...", "deleted": true }

Daily closes

勾稽式:grossSalesCents = cashCents + netsCents + cardCents + deliveryCents + otherCents

方法路径关键参数响应字段
GET/daily-closesquery: clientId?, outletId?, dateFrom?, dateTo?, status?;status=`draftposted
GET/daily-closes/{id}path: daily-close UUIDid, clientId, outletId, closeDate、各渠道金额、payoutCents, payoutNote, varianceCents, status, journalId, sourceFileId
POST/daily-closesbody: clientId, outletId, closeDate、各渠道金额;可选 payoutCents, payoutNote, varianceCents, sourceFileId新建 draft;同一 outlet/date 不可重复
PATCH/daily-closes/{id}上述可变字段至少一项;仅 draft更新后的日结
DELETE/daily-closes/{id}draft{ "id": "...", "deleted": true }
POST/daily-closes/{id}/post无 body;仅已勾稽、非零 draftstatus: "posted" 与生成的 journalId
POST/daily-closes/{id}/reverse无 body;仅 postedstatus: "reversed";关联凭证同步冲销
bash
curl -X POST 'https://books.chuhaisg.com/api/v1/daily-closes' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"CLIENT_UUID","outletId":"OUTLET_UUID","closeDate":"2026-07-22","grossSalesCents":125000,"cashCents":30000,"netsCents":25000,"cardCents":40000,"deliveryCents":30000,"otherCents":0,"payoutCents":0}'

Platform settlements

勾稽式:netPayoutCents = grossCents - commissionCents - deliveryFeeCents - promoCents - refundCents + adjustmentCents

方法路径关键参数响应字段
GET/platform-settlementsquery: clientId?, channel?, status?; status=`draftposted
GET/platform-settlements/{id}path: settlement UUIDid, clientId, outletId, channel, periodStart, periodEnd、各金额、status, journalId, matchedBankTransactionId, sourceFileId, sourceJson
GET/platform-settlements/{id}/checkpath: settlement UUIDexpectedCommissionCents, actualCommissionCents, varianceCents, ruleApplied;无 active rule 时期望值与差异为 null
POST/platform-settlementsbody: clientId, outletId?, channel, periodStart, periodEnd、各金额、可选来源字段新建 draft
PATCH/platform-settlements/{id}可变字段至少一项;仅 draft,日期与金额须重新勾稽更新后的结算
DELETE/platform-settlements/{id}draft{ "id": "...", "deleted": true }
POST/platform-settlements/importbody: clientId, channel, template, csv, outletId?; template=`grabfoodpanda
POST/platform-settlements/{id}/post无 body;仅已勾稽 draftgrossCents > 0netPayoutCents >= 0status: "posted"journalId
POST/platform-settlements/{id}/matchbody: { "bankTransactionId": "UUID" };流水须同客户、未匹配且金额等于 netPayoutCentsstatus: "matched"matchedBankTransactionId
POST/platform-settlements/{id}/unmatch无 body;仅 matched回到 status: "posted",清空匹配流水
POST/platform-settlements/{id}/reverse无 body;必须先 unmatch,且当前为 postedstatus: "reversed";关联凭证同步冲销

导入 body 中的 csv 是 CSV 全文字符串。CSV 金额使用普通十进制格式,导入时转为整数分。三种模板表头必须按以下顺序原样提供;Grab 与 Generic 当前相同:

templateCSV 表头(原样)
grabPeriod Start,Period End,Gross Sales,Commission,Delivery Fee,Promotions,Refunds,Adjustments,Net Payout
foodpandaperiod_start,period_end,gross_amount,commission,delivery_fee,promo,refund,adjustment,payout
genericPeriod Start,Period End,Gross Sales,Commission,Delivery Fee,Promotions,Refunds,Adjustments,Net Payout
bash
curl -X POST 'https://books.chuhaisg.com/api/v1/platform-settlements/import' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"CLIENT_UUID","channel":"grab","template":"grab","csv":"Period Start,Period End,Gross Sales,Commission,Delivery Fee,Promotions,Refunds,Adjustments,Net Payout\n2026-07-01,2026-07-07,1000.00,200.00,20.00,30.00,0.00,0.00,750.00"}'

Settlement rules

commissionBps 为基点,100 表示 1%。tiersJson 可设置阶梯累进佣金:每档只对上一档上限至本档 uptoCents 的 gross 部分应用 bps,最后一档的 uptoCents 必须为 null;各档结果汇总后四舍五入到分。未提供阶梯时按 grossCents × commissionBps / 10000 计算。最终期望佣金为计算值与 minGuaranteeCents 的较大者,即 max(阶梯或固定费率佣金, 保底佣金)

GET /platform-settlements/{id}/check 用同客户、同渠道的 active rule 计算期望佣金,并返回实际佣金及 varianceCents = actualCommissionCents - expectedCommissionCents。导入时,如果 commission 与 active rule 的期望值相差超过期望佣金的 1%,对应 item 会带 warning

方法路径关键参数响应字段
GET/settlement-rulesquery: clientId?规则数组
GET/settlement-rules/{id}path: rule UUIDid, clientId, channel, name, commissionBps, minGuaranteeCents, tiersJson, active 及审计字段
POST/settlement-rulesbody: clientId, channel, name, commissionBps?, minGuaranteeCents?, tiersJson?, active?新建规则;同一客户/channel 只允许一个 active rule
PATCH/settlement-rules/{id}上述可变字段至少一项;minGuaranteeCentstiersJson 可为 null更新后的规则
DELETE/settlement-rules/{id}path: rule UUID{ "id": "...", "deleted": true }

Daily profit report

方法路径关键参数响应字段
GET/reports/daily-profitquery: 必填 clientId;二选一:date,或成对的 dateFrom + dateTo单日返回 date;区间返回 rangetrend。两者都有 total, outlets, unassigned;bucket 含 grossSalesCents, netSalesCents, gstCents, channels, deliverySharePct, platformCostCents, payoutCents, laborCents, netProfitCents
bash
curl 'https://books.chuhaisg.com/api/v1/reports/daily-profit?clientId=CLIENT_UUID&date=2026-07-22' \
  -H "Authorization: Bearer $ACCESS_TOKEN"

7. 供应商对账

供应商对账单以 draft 开始,全部明细清账后变为 reconciled。读取端点可用于当前角色可见的客户;写入、匹配和 reconcile 要求 partner、manager 或 accountant。JSON 金额继续使用整数分

方法路径关键参数语义 / 响应
GET/supplier-statementsquery: clientId?, contactId?, status?;status=`draftreconciled`
GET/supplier-statements/{id}path: statement UUID单个对账单
POST/supplier-statementsbody: clientId, contactId, statementDate, periodStart, periodEnd, totalCents;可选来源字段新建 draft 对账单
PATCH/supplier-statements/{id}至少一个可变字段;仅 draft,status 只能由 reconcile 更改更新后的对账单
DELETE/supplier-statements/{id}draft删除对账单并返回 { "id": "...", "deleted": true }
POST/supplier-statements/importbody: clientId, contactId, statementDate, periodStart, periodEnd, csv新建 draft 与合法明细;返回 { statement, errors: [{row, error}] }
POST/supplier-statements/{id}/auto-match无 body;仅 draft更新后的明细数组;按引用自动匹配非 draft bill
GET/supplier-statements/{id}/reportpath: statement UUID四桶汇总、逐行结果、billsNotOnStatement, statementTotalCents, matchedTotalCents
PATCH/supplier-statements/{id}/lines/{lineId}body: billId(UUID 或 null)、note?;仅 draft手工匹配或清除匹配;金额不同时手工确认必须附 note
POST/supplier-statements/{id}/reconcile无 body;仅 draft只有每行均为 matched 才把状态改为 reconciled;否则返回 400

导入 CSV 的首行必须按顺序为 Date,Doc No,Amount。日期接受该导入器支持的日期格式,金额用普通十进制金额表示并转换为整数分;合法行会保存,错误行列在 errors 中。auto-match 将行的 docRef 先与同客户、同供应商 bill 的 supplierRef 匹配,找不到时再回退到 billNo;同一 bill 不能被多行或其他对账单重复占用。金额相等得到 matched,引用找到但金额不同得到 amountMismatch,找不到可用 bill 得到 missingBill

报告的四个 bucket 是 matchedamountMismatchmissingBillunmatched,每桶都有 countamountCentsbillsNotOnStatement 另列对账期间内尚未被本对账单占用的非 draft bills。reconcile 不过账凭证,也不更改 bill;它表示所有对账行已经清账,并锁定对账单为 reconciled 状态。

bash
curl -X POST 'https://books.chuhaisg.com/api/v1/supplier-statements/import' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"CLIENT_UUID","contactId":"SUPPLIER_UUID","statementDate":"2026-07-31","periodStart":"2026-07-01","periodEnd":"2026-07-31","csv":"Date,Doc No,Amount\n2026-07-10,SUP-INV-42,125.50"}'

bills.supplierRef

Bill 的 supplierRef 是供应商在其单据上给出的引用号,与 KaiderBooks 生成的 billNo 不同。创建或更新 bill 时可传 supplierRef(可选、可为 null,最长 64 字符);对账自动匹配优先使用它,因此应尽量原样保存供应商发票号。billNo 只在 supplierRef 未命中时作为回退键。

8. 成本与盘点

Recipes 与理论毛利

方法路径关键参数语义 / 响应
GET/recipesquery: clientId?, active?配方数组
GET/recipes/marginsquery: 必填 clientId每个配方的 sellPriceCents, theoreticalCostCents, marginCents, marginPct
GET/recipes/{id}path: recipe UUID配方及 components
POST/recipesbody: clientId, name, components;可选 menuCode, sellPriceCents, active新建配方;component 为 { inventoryItemId, qty }
PATCH/recipes/{id}至少一个可变字段;可整体替换 components更新后的配方
DELETE/recipes/{id}path: recipe UUID{ "id": "...", "deleted": true }

qty 是最多四位小数的正数字符串,同一库存项目在一个配方中只能出现一次。GET /recipes/margins 对每个 component 计算 qty × inventoryItem.costCents,每行四舍五入到整数分后求和,得到 theoreticalCostCents。随后计算 marginCents = sellPriceCents - theoreticalCostCentsmarginPct = marginCents / sellPriceCents × 100 并四舍五入到一位小数;没有售价时两个毛利字段为 null,售价为 0 时百分比为 null

Stocktakes 流程

方法路径关键参数语义 / 响应
GET/stocktakesquery: clientId?, outletId?, status?;status=`draftcompleted`
GET/stocktakes/{id}path: stocktake UUID盘点单及其 lines
POST/stocktakesbody: clientId, takenAt, outletId?, note?新建 draft,并把该客户全部有效库存项目及当前 stockQty 快照为明细的 systemQty
PATCH/stocktakes/{id}/lines/{lineId}body: { "countedQty": "12.5000" };仅 draft记录该行实点数量
POST/stocktakes/{id}/complete无 body;仅 draft完成盘点、同步库存,并在存在差异时生成已过账凭证
DELETE/stocktakes/{id}draft{ "id": "...", "deleted": true }

完成时,未填写 countedQty 的行按 systemQty 处理;系统以完成时库存项目的 costCents 计算 (countedQty - systemQty) × costCents 并四舍五入到分,写入 unitCostCentsvarianceCents,再把库存项目的 stockQty 同步为实点数量。盘亏凭证为借记 5010、贷记 1310;盘盈则反向借记 1310、贷记 5010。没有金额差异时不会生成凭证,journalIdnull

9. POS 连接与 Webhook

POS 连接读取要求 settlements.view,写入和 sync-day 要求 settlements.manage。支持的 provider 为 loyverse|epos|ichef|qashier|storehub,status 为 pending|active|disabled

方法路径关键参数语义 / 响应
GET/pos-connectionsquery: clientId?, outletId?, provider?, status?POS 连接数组;不返回 secret
GET/pos-connections/{id}path: connection UUID单个连接,含 webhookPath;不返回 secret
POST/pos-connectionsbody: clientId, provider, outletId?, status?, config?新建连接;响应额外返回 48 字符 webhookSecret,仅此一次
PATCH/pos-connections/{id}body: provider?, outletId?, status?, config?,至少一项更新连接;不能读取或轮换 secret
DELETE/pos-connections/{id}path: connection UUID软删除并返回 { "id": "...", "deleted": true }
POST/pos-connections/{id}/sync-daybody: { "date": "YYYY-MM-DD" };连接必须有 outlet聚合未处理收据并新建或更新 draft 班结单;返回 dailyCloseId, eventsProcessed, grossCents, channels
POST/pos/webhooks/{connectionId}header: x-webhook-secret;body: JSON object公共接收端点;成功返回 { "accepted": true, "eventId": "UUID" }

请在创建连接时立即安全保存 webhookSecret;后续 list、get、update 响应都会移除它。POS 端向 https://books.chuhaisg.com/api/v1/pos/webhooks/{connectionId} 发送该值作为 x-webhook-secret。连接不存在、已 disabled、header 缺失或 secret 错误都统一返回 401 UNAUTHORIZEDInvalid webhook credentials,避免泄露连接是否存在。

收据 payload 示例(type 省略时默认为 receipt):

json
{
  "type": "receipt",
  "receipt_date": "2026-07-22T18:30:00+08:00",
  "total_money": "125.50",
  "payments": [
    { "name": "CASH", "money_amount": "25.50" },
    { "type": "CARD", "money_amount": "100.00" }
  ]
}

这里 total_moneymoney_amount 是 POS payload 的普通十进制金额,不是 Cents 字段;服务端会转换为整数分。sync-day 只处理 eventType: "receipt"、尚未处理且 receipt_date(回退 created_at)日期匹配的事件。每笔 payment 先以 name(回退 type)查找连接 config.paymentMap,映射目标只能是 cash|nets|card|delivery|other;没有有效映射的金额归入 other。系统分别累加收据 total_money 与五个渠道,二者必须相等,否则返回 422;成功后创建或更新对应 outlet/date 的 draft 班结单,并把事件标为已处理。

bash
curl -X POST 'https://books.chuhaisg.com/api/v1/pos/webhooks/CONNECTION_UUID' \
  -H 'Content-Type: application/json' \
  -H 'x-webhook-secret: YOUR_WEBHOOK_SECRET' \
  -d '{"type":"receipt","receipt_date":"2026-07-22","total_money":"125.50","payments":[{"name":"CASH","money_amount":"25.50"},{"name":"CARD","money_amount":"100.00"}]}'

10. 错误处理

所有错误统一包在 error 中;校验错误还可能带 details

json
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Validation failed",
    "details": {}
  }
}
HTTPcode语义
400BAD_REQUESTschema 校验失败、日期区间错误、状态不允许,或日结/平台结算勾稽失败
401UNAUTHORIZED缺少 Bearer token、access token 无效/过期,或 refresh token 缺失/失效
403FORBIDDEN角色门禁、客户数据范围或 permission key 不允许;message 通常指出所需 key
404NOT_FOUND当前 firm/客户范围内找不到资源
409CONFLICT唯一约束冲突,例如同一门店同一天已有日结、门店 code 重复或 active 规则重复

客户端应按 HTTP status 与 error.code 分支;message 面向排错,不应作为稳定的程序判断值。


相关章节: 管理后台中的 API Key 与 Webhook →