CCBill
CCBill 渠道面向你的 App 的网页版售卖通用支付商不接受的内容。顾客在 CCBill 托管的 FlexForm 上付款,钱进你自己的 CCBill 商户账户。Kasskeep 生成支付链接、校验每一条回调、记录结果并发出事件。渠道 id:ccbill,平台:web,data.store: "ccbill"。
能力:订阅、一次性购买、退款、退款查询、主动退款、拒付、对账和测试模式。
开始之前:商户账号
Section titled “开始之前:商户账号”你需要自己的 CCBill 商户账号,Kasskeep 不经手付款。CCBill 在开通前会审核每个网站,请准备好:
- 公司资料和所有者的身份证件。申请里填写的信息必须和证件完全一致。
- 已上线的网站:有年龄确认(18 岁以上)、服务条款、隐私政策、退款与取消政策、客服联系方式;适用时附上准确的 18 U.S.C. 2257 声明。
- 在进入支付页之前清楚标明试用价和续费价;CCBill 支付页上的名称、标志和客服链接必须保留。
- 内容符合 CCBill 的商户可接受使用政策。
详细要求见 CCBill 的商户常见问题和可接受使用政策。通过后你会拿到 6 位客户账号和一个或多个 4 位子账号。
请向 CCBill 商户支持申请两项:
- 为子账号开通动态定价,这样 Kasskeep 才能在链接里设定价格。然后在 CCBill Admin 里复制该子账号的加密密钥(salt)。
- 开一个 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 检查用户是否可用。
2. Webhook 地址
Section titled “2. Webhook 地址”在 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 对每个请求:
- 检查来源地址是否在 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; - 检查
clientAccnum和clientSubacc是你的账号(400 account_mismatch); - 调 Data Link 订阅管理接口,确认这个订阅确实在你的账户下,且能看到回调所说的续费、退款、撤销、拒付、取消或到期。对不上返回
400 unverified_notification,不记账;Data Link 不可用时返回502,以便重新投递; - 按事件类型、订阅号、交易号和时间戳去重;
X-kasskeep_app_id不是本 App 的销售一律忽略;商品按实际支付的价格判定,不信透传字段。
因此伪造一条回调,既要从 CCBill 的网络里发出,又要在你的 CCBill 账户里有对得上的记录。这比签名弱(依赖网络路径和 Data Link 的应答),但这是 CCBill 推荐的做法。校验失败会触发 store.signature_invalid 告警。
3. 商品映射
Section titled “3. 商品映射”把每个商品 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¤cyCode=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¤cyCode=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 设置里允许向表单传入动态价格。
4. 发起结账
Section titled “4. 发起结账”POST /v1/checkoutX-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。