Skip to content

订阅管理 API ​

用户订阅接口,包括 Driver V2 稳定只读摘要,以及网页端查询、变更套餐、取消订阅退款、 取消续订和恢复续订。

路由前缀:/billing

源码:apps/backend/src/routes/billing-subscription.ts


认证 ​

JWT 用户会话可以访问全部下列接口。OAuth 按最小权限拆分:

  • billing:read:只能调用 GET /billing/summary,且只签发给独立环境变量白名单中的官方 Driver V2 client。
  • orders:checkout:保留网页/订单链路的查询和四个写接口,不能读取新的 summary。
Authorization: Bearer <JWT>
Authorization: Bearer wno-...

billing:read V1 只读取用户首次创建的默认个人团队,不支持团队切换。旧查询接口允许 owner 或 accepted member;写入接口仍要求 owner/admin。


端点 ​

网页 Driver 购买资格 ​

GET /billing/driver-offer?teamId=<当前团队ID>,仅 JWT 用户会话可用;OAuth 不支持此接口。必填 teamId,只允许读取本人默认个人团队的资格,响应设置 Cache-Control: no-store。接口不创建订单、不调用支付渠道。

成功返回 { success: true, data },data 包含 teamId、defaultTeamSlug、status、cloudAccess 和 purchase。cloudAccess 为已付权益摘要,停售时仍可读取有效期。非本人默认团队返回 wrong_team 和本人默认团队 slug,不查询其他团队权益;无默认团队返回 no_team。

status 为 available 时,purchase 包含当前数据库实际 productCode、priceCode、currency、amountMinor、billingPeriod,网页用户主动点击后传入现有订阅支付流程。其余状态 not_on_sale、pilot_unavailable、subscription_exists、purchase_conflict 均返回 purchase: null。缺少团队参数返回 400,非 JWT 返回 403,数据库或有效销售价格配置异常返回固定脱敏错误 driver_subscription_unavailable / 503。缺失或未启用的销售配置返回 not_on_sale。

此接口仅供 /o/:teamSlug/settings/billing 的独立 Driver 卡片使用,不依赖平台 billing.plans。下单及支付发起时仍复核资格,不能把只读查询结果当作付款授权。

1. 获取 Driver V2 只读订阅摘要 ​

GET /billing/summary?teamId=<optional>

teamId 仅为未来多团队合同预留。V1 不传时稳定选择 teams.owner_user_id 属于当前用户、按 created_at ASC, id ASC 排序的首个个人团队;传入时必须等于该团队,否则统一返回 404。

响应只包含当前套餐、成交价快照、计费周期、取消语义、权益可用性标志、积分余额、充值档位、最近 3 条订阅账单摘要和网页管理入口,不返回 provider data、支付凭据、卡信息或地址。

credits.* 全部为毫积分(milli-credits,1 积分 = 1000 毫积分),表示账户剩余量;topupTiers 是商品展示额度(沿用配置的用户可见积分单位),两者语义不同。完整字段合同见 docs/features/billing/summary.md。

json
{
  "success": true,
  "data": {
    "teamId": "uuid",
    "plan": {
      "code": "pro",
      "name": "Pro",
      "description": "For individual developers and small teams",
      "creditsPerPeriod": 4500000,
      "features": {
        "status": "available",
        "items": [
          { "included": true, "text": "Up to 10 workflows deployed simultaneously" },
          { "included": false, "text": "Dedicated support" }
        ]
      }
    },
    "price": { "amountMinor": 999, "currency": "USD", "source": "subscription_snapshot" },
    "billingPeriod": "monthly",
    "status": "active",
    "currentPeriodStart": "2026-08-01T00:00:00.000Z",
    "currentPeriodEnd": "2026-09-01T00:00:00.000Z",
    "currentPeriod": {
      "plan": { "code": "pro", "name": "Pro" },
      "price": { "amountMinor": 999, "currency": "USD", "source": "subscription_snapshot" },
      "billingPeriod": "monthly",
      "startsAt": "2026-08-01T00:00:00.000Z",
      "endsAt": "2026-09-01T00:00:00.000Z",
      "creditsPerPeriod": 4500000,
      "creditsGrant": "unknown",
      "source": "subscription_snapshot"
    },
    "nextPeriod": null,
    "expiryBehavior": { "type": "end", "effectiveAt": "2026-09-01T00:00:00.000Z" },
    "renewal": {
      "willRenew": false,
      "cancelAtPeriodEnd": true,
      "cancelEffectiveAt": "2026-09-01T00:00:00.000Z",
      "canResume": true,
      "mode": "manual",
      "autoCharge": false
    },
    "cloudAccess": {
      "status": "inactive",
      "reason": "not_driver_plan",
      "evaluatedAt": "2026-08-25T00:00:00.000Z",
      "validUntil": null,
      "sourcePlanCode": null
    },
    "credits": {
      "totalCredits": 120000,
      "grantCredits": 70000,
      "purchaseCredits": 50000,
      "subscriptionGrantCredits": 40000,
      "topupGrantCredits": 30000,
      "periodEnd": "2026-10-01T00:00:00Z"
    },
    "entitlements": { "status": "unavailable", "items": [] },
    "topupTiers": {
      "status": "available",
      "source": "plan",
      "items": [
        {
          "id": "tier_pro",
          "name": "Pro 专属",
          "priceCents": 2000,
          "baseCredits": 6000,
          "bonusCredits": 600,
          "totalCredits": 6600,
          "bonusPercent": 10,
          "popular": true
        }
      ]
    },
    "recentBillingRecords": [],
    "manageUrl": "https://app.example.com/o/personal-team/settings/billing"
  }
}

状态枚举固定为 none|active|canceled|past_due|trialing|unknown。历史订阅缺少可靠成交币种时 price=null;不会用当前目录价伪造成交价。manageUrl 不携带 OAuth token。

无订阅时 plan=null。介绍优先用 billing.plans 实际配置,再用精确当前订单的冻结描述,最后只读回退同套餐/周期/币种唯一 active 的 product_prices.description。仅历史 currency=null 时,改用已经采用的顶层展示名称匹配同套餐/周期唯一 active 目录文案;名称优先订单商品、其次目录、最后套餐代码。价格仍为 null,不从介绍推断币种。已知币种不跨币种回退;重名多行、未知名称、不支持的非空币种或无真实文字时为 null。权益列表仍只来自 billing.plans,缺配置为 unavailable,不从介绍编造权益。topupTiers 优先用当前套餐专属档位,否则默认档位;credits 无余额记录时为 null,历史细分列 NULL 按 0 呈现。

当前与下一周期 ​

currentPeriod 按组给出 plan:{code,name}、price、billingPeriod、startsAt/endsAt、creditsPerPeriod、creditsGrant、source。每期额度是毫积分,和余额分开;年付不自行乘除 12。订单新期已开始但 cron 未推进时,整组取新期付款事实,不混入旧订阅额度。无精确付款来源的合法历史 Pro 可用 subscription_snapshot 保留真实当前期,不伪造付款。

source 为 payment_order、subscription_snapshot 或仅下期预约使用的 subscription_schedule。未知金额/币种/额度为 null。creditsGrant=granted|pending|unknown 只根据实际发放证据;pending 可显示“本期积分发放中”,不是已到账余额。

nextPeriod 同基础形状,另含 paid:true|false|null、status:scheduled|blocked、reason:string|null,不含 creditsGrant。只有真实已付、连续并允许生效才 scheduled;未付、未知、断档或退款受阻均不能承诺续期。下一期的介绍/权益不重复返回。

expiryBehavior={type,effectiveAt} 的 type 为 end、continue_prepaid、change_plan、none、unknown,分别表示到期结束、同套餐已付延续、已付换套餐、无动作、无法判断。有效时间是下一次周期动作;若保留已付下一期,最终 renewal.cancelEffectiveAt 可以晚于本期结束。所有套餐的 renewal.willRenew 仅表示存在可生效已付下一期,mode 为 prepaid/manual;autoCharge 固定 false,不表示自动扣款。

新后端始终返回 currentPeriod/nextPeriod/expiryBehavior;显式 null 不能从旧字段拼回周期,缺字段才按旧后端兼容。旧平铺套餐/周期保留,介绍仅可用于同 plan code 的新周期。cloudAccess.sourcePlanCode 只在 active 时说明 Driver 权益来源,客户端不自行推断高级包含关系。

plan.creditsPerPeriod 仅在旧套餐代码、计费周期与完整起止时间均和 currentPeriod 相同时返回额度;cron 滞后的窗口为 null,不能把新套餐/年付/周期额度挂在旧名称或月付时间下。详情优先使用完整 currentPeriod。

RPC 团队必须与摘要默认个人团队一致;读取期间团队变更、权威字段缺失、坏日期、负数额度或未知状态枚举时整体返回 BILLING_SUMMARY_FAILED。约定的 null/unknown 表示已核验的不确定事实,不能以缺字段或坏值替代。辅助展示配置仍可局部降级。

完整 JSON 响应预算 32 KiB,兼容旧客户端 64 KiB 上限。文案只在顶层一份,超限展示配置有界裁剪并标记 plan.truncated / topupTiers.truncated,不改变实际金额/周期/状态;正常配置不变,原数据库文案不重写。客户端可通过管理页查看完整介绍。

错误码: 默认个人团队不存在、传入其它团队或任何不可访问团队时均返回 404 BILLING_TEAM_NOT_FOUND;服务端配置或数据库读取失败返回 500 BILLING_SUMMARY_FAILED。

2. 获取原有订阅与积分余额 ​

GET /billing/subscription

获取当前团队的订阅详情和积分余额。

查询参数:

参数类型必填说明
teamIdstring否团队 ID;未提供时稳定回退到默认个人团队

示例请求:

bash
curl -X GET "https://block2-api.wainao.chat/billing/subscription?teamId=xxx" \
  -H "Authorization: Bearer <JWT>"

响应 200 OK:

json
{
  "success": true,
  "data": {
    "subscription": {
      "id": "uuid",
      "planCode": "pro",
      "billingPeriod": "monthly",
      "status": "active",
      "monthlyCredits": 100000,
      "currentPeriodStart": "2026-03-01T00:00:00.000Z",
      "currentPeriodEnd": "2026-04-01T00:00:00.000Z",
      "cancelAtPeriodEnd": false,
      "canceledAt": null,
      "scheduledPlanCode": null
    },
    "balance": {
      "totalCredits": 85000,
      "grantCredits": 80000,
      "purchaseCredits": 5000,
      "periodEnd": "2026-04-01T00:00:00.000Z",
      "periodGrantedCredits": 100000
    }
  }
}

提示

subscription 或 balance 可能为 null(例如新团队尚未创建订阅记录时)。

错误码:

HTTP错误码说明
400NO_TEAM用户不在任何团队中
500—服务端错误

3. 变更套餐 ​

POST /billing/subscription/change-plan

这是兼容的套餐变更意向接口,不收款,也不直接授予新周期。付费目标套餐无论升级还是降级,都必须通过创建订单、支付和履约流程取得权益。

  • action=upgrade:返回创建支付订单的指引。
  • action=downgrade:仅记录未付款的变更意向,不能凭预约字段承诺下一期生效或发放积分。
  • action=none:没有可变更订阅、已是目标套餐,或已有已付套餐预约;不会覆盖已付预约。

目标为 free 时,记录的也是到期结束意向。没有已付下一期且没有待核对付款来源时,现有付费周期结束后停止订阅,不创建免费赠送周期;已经购买的当前期和下一期不被这次请求抹掉,异常付款来源保留待核对。当前套餐或目标套餐是 Driver 时,本接口返回 409 driver_plan_change_unsupported,应使用订阅购买流程。

请求体:

字段类型必填说明
teamIdstring是团队 ID
targetPlanCodestring是目标计划代码(如 pro、free)
billingPeriodstring是计费周期(如 monthly、yearly)

示例请求:

bash
curl -X POST "https://block2-api.wainao.chat/billing/subscription/change-plan" \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"teamId": "xxx", "targetPlanCode": "pro", "billingPeriod": "monthly"}'

响应 200 OK(当前套餐低于 Pro、返回付款指引的示例):

json
{
  "success": true,
  "data": {
    "action": "upgrade",
    "message": "Please create a payment order for upgrade"
  }
}

降档意向同样只返回 action 和 message,例如目标为 free 时返回 action: "downgrade";本接口响应不包含 scheduledPlanCode 或 effectiveAt。周期与到期行为请读取 /billing/summary,不能把 message 当作付款或生效凭据。

错误码:

HTTP错误码说明
400BAD_REQUEST缺少 teamId
400—缺少 targetPlanCode 或 billingPeriod
400NO_TEAM用户不在该团队中
403FORBIDDEN无权限(非 owner/admin)
409driver_plan_change_unsupported当前套餐或目标套餐为 Driver,不支持通过此接口变更
500—服务端错误

4. 取消订阅并退款 ​

POST /billing/subscription/cancel

这是取消已购权益并申请退款的独立流程;cancel-renewal 只记录意向,不退款。当前周期开始未满 7 天时,服务端会固定当前 周期 A 和所有已付款未来周期,先关闭尚可支付的旧入口,再先 future、后 current 地逐笔 全额退款。正好满 7 天即不退款。订单周期无法精确证明时不会猜测或发起部分退款。

网页默认个人团队账单页的独立 Driver 卡片提供此操作,已付有效且停售时仍可申请。必须二次确认,不自动触发或重试;团队或账号变化会关闭旧确认。界面仅以 200 且 status=succeeded 显示完成,202 中的 processing、partially_failed、failed 分别展示实际进度或联系支持的提示,不伪装成退款成功。

billing:read 只有只读权限,不能调用本接口;JWT 或 orders:checkout 仍需通过团队 owner/admin 权限检查。

请求体:

字段类型必填说明
teamIdstring是团队 ID;V1 为默认个人团队
reasonstring否取消原因

处理中返回 202 Accepted,全部同步完成返回 200 OK:

currentPeriodStart、itemCount 和 items 优先采用本次退款推进后的批次明细;零条数和空数组均会保留。最新明细读取失败时会保留启动快照,不能据此判断每笔退款的最新状态。HTTP 状态和顶层 status 仍表示本次推进结果。

json
{
  "success": true,
  "data": {
    "batchId": "00000000-0000-0000-0000-000000000001",
    "status": "processing",
    "currentPeriodStart": "2026-08-01T00:00:00.000Z",
    "itemCount": 2,
    "items": []
  }
}

常见拒绝包括:当前周期已满 7 天、当前付款周期无法证明、存在无法归属的已收款未来 订单。以上情况均不会调用支付渠道。


5. 取消续订 ​

POST /billing/subscription/cancel-renewal

记录取消续订意向,设置 cancelAtPeriodEnd=true。系统没有自动扣款;此操作不退款、不清除已付下一期,也不提前结束已购权益。已付款、未退款且来源与时间连续的下一期仍按原安排生效。

effectiveAt 表示最后连续已购周期的结束时间:没有已付下一期时是当前期末,有有效已付下一期时是下一期末。付款来源失效、断档或待核对时返回 null,不承诺结束时间,也不擅自清理付款事实。没有已付预约、也没有待核对付款来源,且当前期已到期时,本接口会完成到期结束并返回 status=canceled;再次取消已终止订阅为幂等响应。

canResume=true 表示可清除取消意向。付款来源待核对时,它可能为 null,表示时间无法确认,并不等于拒绝恢复;订阅仍为 active 时可调用 resume-renewal 清除意向,结果以该接口返回和 /billing/summary 为准。

请求体:

字段类型必填说明
teamIdstring是团队 ID

示例请求:

bash
curl -X POST "https://block2-api.wainao.chat/billing/subscription/cancel-renewal" \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"teamId": "xxx"}'

响应 200 OK:

json
{
  "success": true,
  "data": {
    "status": "active",
    "cancelAtPeriodEnd": true,
    "effectiveAt": "2026-04-01T00:00:00.000Z",
    "canResume": true,
    "idempotent": false
  }
}

错误码:

HTTP错误码说明
400BAD_REQUEST缺少 teamId
400NO_TEAM用户不在该团队中
403FORBIDDEN无权限(非 owner/admin)
404subscription_not_found订阅不存在
409subscription_not_cancelable当前订阅状态不能取消续订
500—服务端错误

6. 恢复续订 ​

POST /billing/subscription/resume-renewal

清除仍为 active 订阅的取消意向,设置 cancelAtPeriodEnd=false。此操作不扣款、不生成新周期、不发放积分,也不替代付费下单;已有已付下一期保留原安排。取消意向已经清除时可幂等调用。

没有已付预约、也没有待核对付款来源,且当前期已经到期,或订阅不再为 active 时,返回 409 subscription_not_resumable,不会重新激活终态订阅。付款来源待核对时,清除意向不能让受阻预约生效;下一期与到期行为仍须以 /billing/summary 的明确结果为准。

请求体:

字段类型必填说明
teamIdstring是团队 ID

示例请求:

bash
curl -X POST "https://block2-api.wainao.chat/billing/subscription/resume-renewal" \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"teamId": "xxx"}'

响应 200 OK:

json
{
  "success": true,
  "data": {
    "status": "active",
    "cancelAtPeriodEnd": false,
    "effectiveAt": null,
    "canResume": false,
    "idempotent": false
  }
}

错误码:

HTTP错误码说明
400BAD_REQUEST缺少 teamId
400NO_TEAM用户不在该团队中
403FORBIDDEN无权限(非 owner/admin)
404subscription_not_found订阅不存在
409subscription_not_resumable已到期或终态订阅不能恢复
500—服务端错误

消费余额校验 ​

消费扣费在数据库事务中校验正数金额、分项一致性和整笔费用是否足额。余额不足会拒绝扣费,不会把余额扣成负数;拒付不写成功消费流水、不增加 API Key 消耗。重复请求复用原有成功记录,不重复收费。

若提示 balance_inconsistent,表示历史总余额与赠送/购买分项不符,需要根据原流水审计修复,不能通过额外充值或重试掩盖。该错误与正常的 insufficient_credits(可用余额不足)不同。

取消后迟到收款说明 ​

取消完成后,旧付款订单的成功通知仍按原订单退款归属处理。已确认的迟到收款只补退资金, 不重新发放权益,也不影响后来新购的订阅。重复通知与恢复任务复用原批次和在途退款号。 渠道状态无法确认时保留待核对,不能把关单请求成功视为退款或到账成功。

取消中的付款入口待核对 ​

付款入口尚无权威终态时,取消响应保持 processing 并返回 warnings,不得展示成退款完成。payment_entry_unconfirmed 表示渠道意图已记录但入口查询未确认,legacy_payment_intent_unknown 表示历史付款意图未知;原因持久保存于原退款明细 last_error,管理员可按订单检索。一次查无订单不能证明未来没有迟到收款,确认收款后仍按原批次补退。部署时 SUBSCRIPTION_PAYMENT_INTENT_CLOSURE_ENABLED 默认关闭,只有共库节点旧支付进程和请求全部耗尽、并获配置批准后才能启用新单的本地关单证明;历史未知订单不会回填。

AI Workflow Editor