跳转到内容

CCBill

CCBill 渠道面向你的 App 的网页版售卖通用支付商不接受的内容。顾客在 CCBill 托管的 FlexForm 上付款,钱进你自己的 CCBill 商户账户。Kasskeep 生成支付链接、校验每一条回调、记录结果并发出事件。渠道 id:ccbill,平台:web,data.store: "ccbill"。

能力:订阅、一次性购买、退款、退款查询、主动退款、拒付、对账和测试模式。

你需要自己的 CCBill 商户账号,Kasskeep 不经手付款。CCBill 在开通前会审核每个网站,请准备好:

  • 公司资料和所有者的身份证件。申请里填写的信息必须和证件完全一致。
  • 已上线的网站:有年龄确认(18 岁以上)、服务条款、隐私政策、退款与取消政策、客服联系方式;适用时附上准确的 18 U.S.C. 2257 声明。
  • 在进入支付页之前清楚标明试用价和续费价;CCBill 支付页上的名称、标志和客服链接必须保留。
  • 内容符合 CCBill 的商户可接受使用政策。

详细要求见 CCBill 的商户常见问题和可接受使用政策。通过后你会拿到 6 位客户账号和一个或多个 4 位子账号。

请向 CCBill 商户支持申请两项:

  1. 为子账号开通动态定价,这样 Kasskeep 才能在链接里设定价格。然后在 CCBill Admin 里复制该子账号的加密密钥(salt)。
  2. 开一个 Data Link 用户(用户名和密码),用于订阅管理接口。Data Link 只接受登记过的地址,需要把你的 Kasskeep 服务的出口 IP 告诉 CCBill。
终端窗口
curl -X PUT https://api.kasskeep.com/v1/admin/tenants/ten_xxx/apps/app_xxx/credentials/ccbill \
-H "Authorization: Bearer kkp_..." -H "Content-Type: application/json" \
-d '{"values": {"client_accnum": "900100", "client_subacc": "0000", "salt": "...",
"datalink_username": "...", "datalink_password": "..."}}'
键 必填 含义
client_accnum 是 6 位客户账号
client_subacc 是 FlexForm 所属的 4 位子账号
salt 动态定价时 子账号加密密钥,用来计算 formDigest
datalink_username、datalink_password 是 Data Link 用户,用于回查确认回调、退款和对账
webhook_ips 否 允许的回调来源,逗号分隔的 IP 或 IPv4 CIDR;写 ccbill 表示 CCBill 公布的地址段(默认)
environment 否 production(默认);CCBill 测试账号用 sandbox

凭据用租户密钥加密存储,接口不返回明文,控制台只显示「已配置 · 末 4 位」。POST .../credentials/ccbill/verify 会调用一次 Data Link 检查用户是否可用。

在 CCBill Admin 里给子账号添加 webhook:

  • 地址:https://api.kasskeep.com/v1/stores/ccbill/<app_id>/webhook(App 的 ccbill_webhook_path)。
  • 格式:JSON 或 URL 编码都可以。
  • 事件:NewSaleSuccess、NewSaleFailure、RenewalSuccess、RenewalFailure、Cancellation、Expiration、Refund、Return、Void、Chargeback。其他事件记为 unrecognized 并忽略。

CCBill 的 webhook 没有签名。Kasskeep 对每个请求:

  1. 检查来源地址是否在 CCBill 公布的地址段 64.38.212.0/24、64.38.215.0/24、64.38.240.0/24、64.38.241.0/24 内(或你配置的 webhook_ips),否则返回 403 ip_not_allowed;
  2. 检查 clientAccnum 和 clientSubacc 是你的账号(400 account_mismatch);
  3. 调 Data Link 订阅管理接口,确认这个订阅确实在你的账户下,且能看到回调所说的续费、退款、撤销、拒付、取消或到期。对不上返回 400 unverified_notification,不记账;Data Link 不可用时返回 502,以便重新投递;
  4. 按事件类型、订阅号、交易号和时间戳去重;
  5. X-kasskeep_app_id 不是本 App 的销售一律忽略;商品按实际支付的价格判定,不信透传字段。

因此伪造一条回调,既要从 CCBill 的网络里发出,又要在你的 CCBill 账户里有对得上的记录。这比签名弱(依赖网络路径和 Data Link 的应答),但这是 CCBill 推荐的做法。校验失败会触发 store.signature_invalid 告警。

把每个商品 id 映射到一个 FlexForm 和价格。外部 id 是 FlexForm id,后面可以跟 CCBill 的动态定价参数:

终端窗口
# 订阅:每 30 天 9.99 美元,直到取消
npx @kasskeep/cli products set --id app_xxx --channel ccbill --product-id pro_monthly --type subscription \
--external-id "687fa3e0-e60d-4466-88e2-181fa56dd6a9?initialPrice=9.99&initialPeriod=30&recurringPrice=9.99&recurringPeriod=30&numRebills=99&currencyCode=840"
# 一次性:4.99 美元,CCBill 那边的访问期 2 天
npx @kasskeep/cli products set --id app_xxx --channel ccbill --product-id gems_100 --type one_time \
--external-id "687fa3e0-e60d-4466-88e2-181fa56dd6a9?initialPrice=4.99&initialPeriod=2&currencyCode=840"
# 价格在表单里设定
npx @kasskeep/cli products set --id app_xxx --channel ccbill --product-id vip --type subscription --external-id "cb617dcc-8467-49ab-b3a7-735ce1d60ad9"
  • 价格保留两位小数,周期以天计(1 到 365),numRebills 为 1 到 99(99 表示直到取消)。
  • currencyCode 用 ISO 数字代码:840 美元、978 欧元、826 英镑、124 加元、036 澳元、392 日元。
  • 订阅必须带齐三个续费参数;一次性商品不能带。
  • 在 FlexForm 设置里允许向表单传入动态价格。
POST /v1/checkout
X-Kasskeep-Key: pk_...
{"app_user_id": "6f1c...", "channel": "ccbill", "product_id": "pro_monthly",
"success_url": "https://play.example.com/done", "cancel_url": "https://play.example.com/shop"}

返回 {"url": "https://api.ccbill.com/wap-frontflex/flexforms/<flex id>?...", "session_id": "ccbo_..."}。链接里带 clientSubacc、价格、formDigest(价格参数加 salt 的 MD5),以及透传字段 X-kasskeep_app_id、X-kasskeep_app_user_id、X-kasskeep_product_id、X-kasskeep_order_id(即 session_id)。

  • 付款后 CCBill 把顾客带到 FlexForm 上设置的成功或失败页面;success_url 和 cancel_url 不会传给 CCBill。
  • quantity 只能是 1。
  • 从表单返回不代表已付款;以回调或 GET /v1/customers 为准开通权益。
  • original_transaction_id 是 CCBill 订阅号;每次扣款是一笔交易,以 CCBill 交易号为键。
  • price_micros 和 currency 是顾客实际被扣的金额。付给你的美元金额(accountingAmount)另行保存,用于流水统计。
CCBill 回调 事件
NewSaleSuccess purchase(一次性)或 subscription.started
NewSaleFailure 只记录
RenewalSuccess subscription.renewed;续费失败后恢复时带 data.recovered
RenewalFailure subscription.billing_issue;宽限期到 nextRetryDate
Cancellation subscription.canceled(权益保留到本期结束)
Expiration subscription.expired
Refund、Return refund;部分退款只通知(data.partial: true),全额退款收回权益
Void revoked,data.revocation: "void"
Chargeback chargeback(data.final: true)和 refund(data.reason: "chargeback"),收回权益
终端窗口
curl -X POST https://api.kasskeep.com/v1/admin/tenants/ten_xxx/apps/app_xxx/channels/ccbill/refunds \
-H "Authorization: Bearer kkp_..." -H "Content-Type: application/json" \
-d '{"transaction_id": "0912191101000000159", "amount_micros": 5000000}'

只有所有者和管理员可用(refunds.write)。Kasskeep 调用 Data Link 的 refundTransaction;不传 amount_micros 即全额退款。CCBill 只退订阅的最近一笔扣款,并会同时取消订阅,所以更早的扣款返回 409 refund_not_latest,请到 CCBill Admin 里退。退款在 CCBill 的 Refund 回调到达时入账。

Kasskeep 每天向 Data Link 查询每个未结束的 CCBill 订阅和近期一次性购买的状态(每个 App 最多 200 个)。漏掉的到期或取消会补记并发出事件;CCBill 那边的续费、退款、撤销或拒付次数多于 Kasskeep 记录的,会以 reconcile.failed 告警列出订阅号。

打开测试商店后,不需要 CCBill 账户也能模拟 CCBill 通知。它们走同一套记账和事件,environment 为 "test":

终端窗口
curl -X POST https://api.kasskeep.com/v1/admin/tenants/ten_xxx/apps/app_xxx/channels/ccbill/simulate \
-H "Authorization: Bearer kkp_..." -H "Content-Type: application/json" \
-d '{"event_type": "NewSaleSuccess", "app_user_id": "6f1c...", "product_id": "pro_monthly"}'

event_type 可以是上表中任一事件。之后的事件传 subscription_id 或 app_user_id(取该用户最近一笔模拟购买),退款可以传 amount_micros。对模拟交易发起退款时,会模拟一条 Refund,不调用 CCBill。