开发者 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 | 字符串;具体可排序字段由资源决定,常见降序写法为 -field 或 field:desc |
q | 支持该参数的列表端点用于文本搜索 |
典型分页响应为 { "items": [...], "total": 42 }。清算中心的列表端点目前返回数组,并使用各自的筛选参数,不套用通用分页。
2. 认证与会话
登录换取 JWT
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"}'成功响应包含 accessToken、user、firm、role、clientId 与 firms。把 access token 放进后续请求:
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。
curl -X POST 'https://books.chuhaisg.com/api/v1/auth/refresh' \
-b cookies.txt -c cookies.txtAPI 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. 权限模型
事务所有四个内部角色:partner、manager、accountant、intern。另有面向门户的 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 |
| 管理 Webhook | webhooks.manage | 管理端点还要求 partner 或 manager |
其余 key 覆盖账簿编辑、复核、自动化、账单审批、工资、客户、用户、角色、API Key、审计、发票、付款、期间锁、机构设置、合规和公司秘书等功能。缺少所需 key 返回 403 FORBIDDEN,message 会指出 key,例如 Permission denied: settlements.manage。
4. Webhook
管理端点与事件
GET/POST /webhooks、PATCH/DELETE /webhooks/{id} 和 POST /webhooks/{id}/test 需要 partner 或 manager 角色以及 webhooks.manage。
创建时提交 url、至少一个 events 字符串和可选的 status(active 或 disabled)。当前事件订阅值是开放字符串,不是固定 enum;管理界面的常见业务值为 invoice.sent。代码中唯一固定并实际投递的测试事件为 webhook.test。
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:id、event: "webhook.test"、createdAt、firmId,header 为 Content-Type: application/json 与 x-kaiderbooks-event: webhook.test,超时 5 秒。API 返回 delivered、status、statusCode。
签名校验
当前实现不生成 secret,也不发送签名或时间戳 header,因此没有可执行的签名校验流程。接收端现阶段只能把 x-kaiderbooks-event 用于路由,不能用它证明来源;应使用专用 HTTPS URL、网络层访问控制与幂等处理,并等待服务端增加签名协议后再信任业务投递。
5. 端点总表
下表路径均相对于 https://books.chuhaisg.com/api/v1。
| 模块 | 基础路径 | 主要操作 |
|---|---|---|
| auth | /auth | login、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/webhooks | POS 连接 CRUD、收据 Webhook、按日同步班结单 |
| reports | /reports | trial balance、general ledger、P&L、balance sheet、cashflow、GST F5、daily profit 与导出 |
| filing | /filing-deadlines、/filing-records、/reminder-rules | 截止日、提交记录、提醒规则 |
| payroll | /payroll-runs | payroll run 与 payslip CRUD、complete |
| compliance | /compliance | overview、risk assessments、beneficial owners、CDD、monitoring reviews、STR reports |
6. 清算中心端点详表
所有清算读取需要 settlements.view,写入需要 settlements.manage。列表可省略 clientId 以查询当前 firm;client 身份始终被限制到自己的 clientId。下列所有 JSON 金额均为整数分。
Outlets
| 方法 | 路径 | 关键参数 | 响应字段 |
|---|---|---|---|
| GET | /outlets | query: clientId? | 数组;id, clientId, code, name, address, active 及审计字段 |
| GET | /outlets/{id} | path: outlet UUID | 单个 outlet |
| POST | /outlets | body: 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-closes | query: clientId?, outletId?, dateFrom?, dateTo?, status?;status=`draft | posted |
| GET | /daily-closes/{id} | path: daily-close UUID | id, clientId, outletId, closeDate、各渠道金额、payoutCents, payoutNote, varianceCents, status, journalId, sourceFileId |
| POST | /daily-closes | body: 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;仅已勾稽、非零 draft | status: "posted" 与生成的 journalId |
| POST | /daily-closes/{id}/reverse | 无 body;仅 posted | status: "reversed";关联凭证同步冲销 |
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-settlements | query: clientId?, channel?, status?; status=`draft | posted |
| GET | /platform-settlements/{id} | path: settlement UUID | id, clientId, outletId, channel, periodStart, periodEnd、各金额、status, journalId, matchedBankTransactionId, sourceFileId, sourceJson |
| GET | /platform-settlements/{id}/check | path: settlement UUID | expectedCommissionCents, actualCommissionCents, varianceCents, ruleApplied;无 active rule 时期望值与差异为 null |
| POST | /platform-settlements | body: clientId, outletId?, channel, periodStart, periodEnd、各金额、可选来源字段 | 新建 draft |
| PATCH | /platform-settlements/{id} | 可变字段至少一项;仅 draft,日期与金额须重新勾稽 | 更新后的结算 |
| DELETE | /platform-settlements/{id} | 仅 draft | { "id": "...", "deleted": true } |
| POST | /platform-settlements/import | body: clientId, channel, template, csv, outletId?; template=`grab | foodpanda |
| POST | /platform-settlements/{id}/post | 无 body;仅已勾稽 draft,grossCents > 0 且 netPayoutCents >= 0 | status: "posted" 与 journalId |
| POST | /platform-settlements/{id}/match | body: { "bankTransactionId": "UUID" };流水须同客户、未匹配且金额等于 netPayoutCents | status: "matched" 与 matchedBankTransactionId |
| POST | /platform-settlements/{id}/unmatch | 无 body;仅 matched | 回到 status: "posted",清空匹配流水 |
| POST | /platform-settlements/{id}/reverse | 无 body;必须先 unmatch,且当前为 posted | status: "reversed";关联凭证同步冲销 |
导入 body 中的 csv 是 CSV 全文字符串。CSV 金额使用普通十进制格式,导入时转为整数分。三种模板表头必须按以下顺序原样提供;Grab 与 Generic 当前相同:
| template | CSV 表头(原样) |
|---|---|
grab | Period Start,Period End,Gross Sales,Commission,Delivery Fee,Promotions,Refunds,Adjustments,Net Payout |
foodpanda | period_start,period_end,gross_amount,commission,delivery_fee,promo,refund,adjustment,payout |
generic | Period Start,Period End,Gross Sales,Commission,Delivery Fee,Promotions,Refunds,Adjustments,Net Payout |
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-rules | query: clientId? | 规则数组 |
| GET | /settlement-rules/{id} | path: rule UUID | id, clientId, channel, name, commissionBps, minGuaranteeCents, tiersJson, active 及审计字段 |
| POST | /settlement-rules | body: clientId, channel, name, commissionBps?, minGuaranteeCents?, tiersJson?, active? | 新建规则;同一客户/channel 只允许一个 active rule |
| PATCH | /settlement-rules/{id} | 上述可变字段至少一项;minGuaranteeCents、tiersJson 可为 null | 更新后的规则 |
| DELETE | /settlement-rules/{id} | path: rule UUID | { "id": "...", "deleted": true } |
Daily profit report
| 方法 | 路径 | 关键参数 | 响应字段 |
|---|---|---|---|
| GET | /reports/daily-profit | query: 必填 clientId;二选一:date,或成对的 dateFrom + dateTo | 单日返回 date;区间返回 range 与 trend。两者都有 total, outlets, unassigned;bucket 含 grossSalesCents, netSalesCents, gstCents, channels, deliverySharePct, platformCostCents, payoutCents, laborCents, netProfitCents |
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-statements | query: clientId?, contactId?, status?;status=`draft | reconciled` |
| GET | /supplier-statements/{id} | path: statement UUID | 单个对账单 |
| POST | /supplier-statements | body: 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/import | body: 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}/report | path: 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 是 matched、amountMismatch、missingBill 和 unmatched,每桶都有 count 与 amountCents。billsNotOnStatement 另列对账期间内尚未被本对账单占用的非 draft bills。reconcile 不过账凭证,也不更改 bill;它表示所有对账行已经清账,并锁定对账单为 reconciled 状态。
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 | /recipes | query: clientId?, active? | 配方数组 |
| GET | /recipes/margins | query: 必填 clientId | 每个配方的 sellPriceCents, theoreticalCostCents, marginCents, marginPct |
| GET | /recipes/{id} | path: recipe UUID | 配方及 components |
| POST | /recipes | body: 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 - theoreticalCostCents,marginPct = marginCents / sellPriceCents × 100 并四舍五入到一位小数;没有售价时两个毛利字段为 null,售价为 0 时百分比为 null。
Stocktakes 流程
| 方法 | 路径 | 关键参数 | 语义 / 响应 |
|---|---|---|---|
| GET | /stocktakes | query: clientId?, outletId?, status?;status=`draft | completed` |
| GET | /stocktakes/{id} | path: stocktake UUID | 盘点单及其 lines |
| POST | /stocktakes | body: 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 并四舍五入到分,写入 unitCostCents 与 varianceCents,再把库存项目的 stockQty 同步为实点数量。盘亏凭证为借记 5010、贷记 1310;盘盈则反向借记 1310、贷记 5010。没有金额差异时不会生成凭证,journalId 为 null。
9. POS 连接与 Webhook
POS 连接读取要求 settlements.view,写入和 sync-day 要求 settlements.manage。支持的 provider 为 loyverse|epos|ichef|qashier|storehub,status 为 pending|active|disabled。
| 方法 | 路径 | 关键参数 | 语义 / 响应 |
|---|---|---|---|
| GET | /pos-connections | query: clientId?, outletId?, provider?, status? | POS 连接数组;不返回 secret |
| GET | /pos-connections/{id} | path: connection UUID | 单个连接,含 webhookPath;不返回 secret |
| POST | /pos-connections | body: 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-day | body: { "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 UNAUTHORIZED 和 Invalid webhook credentials,避免泄露连接是否存在。
收据 payload 示例(type 省略时默认为 receipt):
{
"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_money 与 money_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 班结单,并把事件标为已处理。
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:
{
"error": {
"code": "BAD_REQUEST",
"message": "Validation failed",
"details": {}
}
}| HTTP | code | 语义 |
|---|---|---|
| 400 | BAD_REQUEST | schema 校验失败、日期区间错误、状态不允许,或日结/平台结算勾稽失败 |
| 401 | UNAUTHORIZED | 缺少 Bearer token、access token 无效/过期,或 refresh token 缺失/失效 |
| 403 | FORBIDDEN | 角色门禁、客户数据范围或 permission key 不允许;message 通常指出所需 key |
| 404 | NOT_FOUND | 当前 firm/客户范围内找不到资源 |
| 409 | CONFLICT | 唯一约束冲突,例如同一门店同一天已有日结、门店 code 重复或 active 规则重复 |
客户端应按 HTTP status 与 error.code 分支;message 面向排错,不应作为稳定的程序判断值。