IAP

苹果内购(IAP)从入门到精通(9)- StoreKit2 用户退款与客诉处理优化

苹果内购(IAP)从入门到精通(9)- StoreKit2 用户退款与客诉处理优化

Posted by WTJ on July 9, 2022

WWDC21 新特性与痛点优化

WWDC21 苹果针对该 Session,为我们带来了以下四类后台 API:

  • Invoice lookup API
  • Refunded purchases API
  • Renewal extension API
  • Consumption API

以及两类客户端 StoreKit 2 API:

  • Manage subscriptions API
  • Request refund API

本节会结合现状痛点,分别对上述新特性进⾏逐⼀解析

1、Invoice lookup API ⸺苹果订单关联能⼒优化,可⽤于搭建客诉平台

⻓期以来,IAP 的客诉处理⼀直⼀⾔难尽。其根本原因在于我们没有办法将⽤户⽀付后获得的苹果订单与业务订单关 联起来。主要痛点表现为以下两⽅⾯:

  • ⽆法判断⽤户提供的苹果订单是否已经发货:由于⽤户提供的只有苹果订单相关的信息,我们没有办法利⽤苹
  • 果订单反查出业务订单。所以灰产很有可能多次利⽤同⼀截图,进⾏骗补的操作
  • ⽆法判断⽤户提供的苹果订单截图的真实性:我们团队有幸⻅识到 PhotoShop ⾼⼿⾼仿订单截图,从那以后,
  • 我们遇到类似的场景,只能引导⽤户联系苹果进⾏退款处理。⽽苹果的退款流程也是⼀⾔难尽,⼤⼤降低了⽤
  • 户体验充值的乐趣,同时也误伤了⼀部分真实⽤户(不允许退款,但确实未发货的⽤户)

WWDC21,苹果终于提出了解法:Invoice lookup API。我们未来可以直接通过⽤户反馈的苹果订单,关联到具体的业 务订单,⼤概的流程如下:

1.1 引导⽤户提供苹果订单截图 / 直接引导⽤户提供苹果订单中的 Invoice Order ID(如下图)

428b5b9f7a3045e4ac815c2564fad020~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

(若⽤户提供是苹果订单截图,我们可以通过图像识别 / ⼈⼯客服提取出 Invoice Order ID)

1.2 通过 Invoice lookup API 进⾏查询,其返回的 JWS 形式的票据内容

46c79fd1bb0f48f5802b7f160bdbd322~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

1.3 解码 JWS 形式的票据,获得苹果唯⼀订单标示(originalTransactionId),继⽽关联⾄业务订单;若是通过

StoreKit 2 ⽀付的订单,还可获取稳定透传字段(appAccountToken),⽤于映射关联⽤户

1.4 最终可实现对⽤户进⾏查单 / 智能补发等操作

46c79fd1bb0f48f5802b7f160bdbd322~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

Invoice lookup API 需要提供 {customer_order_id}(也即上⽂中搜集的 Invoice Order ID)与 {appAppleId}
  • API:/inApps/v1/lookup/{customer_order_id}
  • 调⽤结果如下图示:

b31171e0be024c14abcbe81af8508e7f~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

此处需要注意的是,status 包含以下值:

  • 0:合法订单
  • 1:⾮法订单
  • 2:虽然订单合法,但是没有找到可分享给开发者的交易信息(理论上仅少概率出现,⼀般出现在某些⽐较重视
  • 隐私保护的国家。该部分国家不允许苹果与开发者分享此类较敏感信息;由于⽬前正式⽂档尚未公布,个⼈建
  • 议待苹果正式上线相关功能后,对该错误码进⾏监控搜集

需要注意的是,苹果建议我们把⽤户已经反馈的 Invoice Order ID 存储⾄业务订单中(如下图示)

dbc7b0f769ec46e6aca73a2d72899c7e~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

2、Refunded purchases API ⸺对历史退款订单进⾏索回

⼀直以来,IAP ⼀直存在退款灰产。在 WWDC20 之前,⽤户退款后,我们是不会收到退款通知的。也就是说,⽤户 退款对我们⽽⾔,是⽆感知的。但是问题来了,我们的货早已在票据校验后发出,如果我们不知道⽤户退款了,⼜怎 么样对发出的货进⾏回收呢?

答案是没办法回收,这也就导致了货发了,钱没收到。 由此也诞⽣了⼀条 IAP 退款灰产链:灰产使⽤⾃身苹果AppleID 给其他⽤户进⾏代充,代充完毕后再前往苹果进⾏退款;或直接提供代退款服务。苹果为了打击这条灰产链,每年都有对⽤户退款进⾏收紧,并终于,在去年正式发布退款通知。⾄此,我们才能感知⽤户退款这⼀事件,以 对已发的货进⾏索回

当前苹果提供的退款相关能⼒只有退款通知(REFUND)。该通知基本上能够覆盖⻛控索回、数据分析等场景。但是我们作为⽀付平台,在推进业务接⼊退款通知时,往往会遇到以下问题:

  • 不少业务已经上线很多年,甚⾄开发团队已经解散,只有外包团队在维护。这部分团队没有能⼒接⼊退款通知。但是由于恶意退款,灰产仍旧对业务造成了不少的损失

  • 业务从上线到退款通知接⼊,往往有⼀个⽐较⻓的间隔,这段期间会产⽣⼤量的恶意退款订单,我们没有办法对这部分订单进⾏索回

WWDC21,苹果为我们提供了 Refunded purchases API,⽤于主动判断某笔订单是否退款。有了该接⼝,我们就可以起⼀个任务,遍历所有的历史订单,对未接⼊退款通知 / 接⼊退款通知前的退款订单进⾏索回

Refunded purchases API 需要提供订单唯⼀标示 {original_transaction_id} 与 {appAppleId}。类似的,该接⼝返回包含多笔 JWS 形式的票据内容的数组,意味着我们同样可以获得苹果唯⼀订单标示(originalTransactionId)以及稳定透 传字段(appAccountToken)

81723f303803410e8e6c117cf8579312~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

  • API:/inApps/v1/refund/lookup/{original_transaction_id}
  • 调⽤结果如下图示:

18d8a75afbac4c3ab9cb1e68f59d6e2f~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

99396d477439490aa9e2a8524dcc1c26~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

需要注意的是,苹果建议将我们把 PurchaseDate、RefundDate 进⾏存储(如下图示)

3、Renewal extension API ⸺赠送⼀定的⾃动续期订阅时⻓,⽤于挽留⽤户

158d7ec6a398448bacdb038f124eab7a~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

众所周知,因为⼀些原因,NBA 在中国停⽌播放了⼀段时间。但在 NBA 停播之前,不少⽤户为了观赛购买了⼀些体 育类应⽤的会员。现在 NBA 停赛了,业务希望赠送⽤户⼀定的包⽉时⻓以补偿⽤户。那么此时业务只能搜集这批⽤ 户,在业务数据库进⾏时⻓增加的操作

但这会产⽣⼀个问题,假设⽤户之前开通的是连续包⽉(对应苹果物品类型为⾃动续期订阅)。那么哪怕这个业务给 ⽤户赠送了两个⽉的时⻓,根据 IAP 的规则,他仍旧会每⽉扣费。这就导致⼀个问题,⽤户永远有多出的两个⽉时 ⻓,⽽且没办法进⾏消耗。出现这种状况,⽤户只能退订(取消⾃动续期订阅),才能够把赠送的两个⽉时⻓给消耗 掉

但新的问题⼜来了,我们作为开发者,肯定不希望⽤户关闭连续包⽉。更何况⽤户要是连续⼀年购买同⼀订阅组的⾃ 动续期订阅,我们还能享受 85% 的分成(对⽐普通购买仅能享受 70% 的分成)。但这种场景,⽬前确实⽆解,只能 让⽤户取消断档,才能处理⽤户的投诉。

终于在 WWDC21,苹果为我们提供了新的思路。苹果为我们新增了⼀个 Renewal extension API,⽤于免费延⻓⾃动 续期订阅的时⻓。通过该接⼝延⻓续费时⻓,能够解决我们上述场景中遇到的问题,即在⽤户不断档(取消⾃动续期 订阅)的情况下,赠送⽤户可消耗完的订阅时⻓

同时,该接⼝也可以⽤于⽤户促销(可以结合不同业务的场景,免费给⽤户赠送⼀定的订阅时⻓)

Renewal extension API 需要提供订单唯⼀标示 {original_transaction_id} 、延⻓时间 {extendByDays} 与延⻓原因
{extendReasonCode}
  • API:/inApps/v1/subscription/extend/{original_transaction_id}
  • 调⽤结果如下图示:

2265203f29ba4aa5ae3c25219583e8a9~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

需要注意的是,该接⼝有以下限制:

  • 每次最多延⻓ 90 天
  • ⼀年针对具体某个订阅,最多延⻓两次
  • 延⻓的时间不算在 85% 分成的⼀年时⻓

d1c42b565950469e868a6cf6662186b6~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

开发者服务中断或宕机,导致用户无法使用服务,开发者主动给用户进行补偿。流程图已经很清晰的表达了,这里就不解析了。

370f9aaa794f4d5e9b3f97f64434f273~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

开发者主动取消了活动,给用户发补偿。

4、Consumption API ⸺退款前置通知(已上线)

a721b6f02d0249d7a9a5eef0942277a8~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

⼀直以来,⽤户恶意退款都是重要的 IAP 灰产获利⽅式之⼀。好消息是,苹果也发现了这个问题,并⼀直不断在优化关于退款处理的逻辑。从⼀开始的开发者⽆感知,到上⼀年新特性退款通知,再到今年的新特性,都在不断降低恶意退款的占⽐

上⼀年发布的退款通知,虽然可以让开发者感知⽤户退款事件以进⾏索回。但仍存在⼀个灰产场景:如果⽤户购买的是应⽤内的货币,将其⽴即消耗完后再退款,此时我们是没办法做有效的索回的

典型的场景是,灰产通过 AppleID 购买王者荣耀点券。然后其在电商平台上销售⽪肤,等找到买家后,通过赠送的⼿段,使⽤点券赠送⽪肤⾄买家,并将账号上剩余点券消耗完毕。此时,灰产再通过苹果退款,获取退款款项,我们作为开发者,收到了苹果退款通知。但因为灰产的账号点券余额已为零,我们没办法对其进⾏有效索回,只能将其扣为负值,待其充值后再进⾏索回。但这⼜引发了⼀个问题,灰产所使⽤的游戏内账号,⼤部分都是通过养号获得的⼀次性账号。灰产不关⼼账号余额会不会被扣⾄负值,哪怕账号被封了也⽆所谓。毕竟,灰产下次也是⽤其他王者账号进 ⾏代充退款操作

类似的问题,在其他不同场景下也有很多案例。

关键原因在于退款审核是由苹果处理的,但苹果并没有开发者侧的账号信息,所以苹果只能单纯根据当前 AppleID 的⻛控情况进⾏退款审核判断

为了解决该问题,苹果在 WWDC21 新增了 Consumption API。需要注意的是,这⾥提到的 Consumption API 有两类 定义:

  • 狭义的 Consumption API:我们通过 API 把业务侧信息告知苹果时调⽤的接⼝
  • ⼴义的 Consumption API:既包含狭义的 Consumption API,还包括 CONSUMPTION_REQUEST 通知

苹果⽬前在⽂档中使⽤的定义是⼴义的 Consumption API,即包含了 CONSUMPTION_REQUEST 通知,所以本⽂之后的 Consumption API 含义与苹果保持⼀致,即指⼴义的 Consumption API

整个 Consumption API 实际上就是为我们提供了⼀个退款前置查询的能⼒,其整个交互过程如下:

  • 1、⽤户退款
  • 2、苹果收到退款请求后,会在 48 ⼩时内进⾏审核,同时发送 CONSUMPTION_REQUEST 通知⾄我们
  • 3、我们收到 CONSUMPTION_REQUEST 通知后,需要在 12 ⼩时内,调⽤狭义的 Consumption API 进⾏请求,告知苹果⽤户信息
  • 4、苹果根据我们反馈的信息,结合 AppleID 信息,对退款⽤户进⾏审核(需要注意的是,苹果不会完全采信我们信息,仅⽤做参考)
  • 5、苹果同意退款
  • 6、我们收到退款通知后,给予苹果正常回包,并进⾏索回等操作 623d09becdee4ae8897b351176a76325~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

狭义的 Consumption API 需要提供下列⼀些⽤户状态相关的信息:

  • accountTenure:⽤户账号年龄(推测⽤于未成年退款判断)
  • appAccountToken:⽤户账号的唯⼀ UUID 映射值,建议在后台建⽴并存储 UUID 与⽤户账号的映射
  • consumptionStatus:表示⽤户对购买商品的消耗程度

0:暂未定义 1:商品未消耗 2:商品部分消耗 3:商品完全消耗(当出现商品从 A 账号转移⾄ B 账号时,也认为是该状态)

  • customerConsented:表示⽤户是否同意分享商品消耗数据,建议根据⽤户是否同意隐私协议传参。若⽤户不
  • 同意,该接⼝其他字段可不传⼊
  • deliveryStatus:商品是否正常发货
  • lifetimeDollarsPurchased:⽤户全平台累计付款⾦额(货币单位:美元)
  • lifetimeDollarsRefunded:⽤户全平台累计退款⾦额(货币单位:美元)
  • platform:⽤户对商品进⾏消耗的平台

0:暂未定义 1:苹果平台 2:⾮苹果平台

  • playTime:⽤户累计使⽤时⻓

    0:暂未定义 1:五分钟以内 2:⼀⼩时以内 3:六⼩时以内 4:⼀天以内 5:四天以内 6:⼗六天以内 7:超过⼗六天

  • sampleContentProvided:购买前是否提供免费试⽤ / 购买后获得的商品样例,以及是否介绍商品具体作⽤

  • userStatus:⽤户账号状态

0:暂未定义 1:可⽤ 2:冻结 3:封禁 4:部分限制

除了上述状态信息外,该接⼝同样需要提供订单唯⼀标示 {original_transaction_id}

deeff5ba3d1f4b7383a9e262f4cd3b9f~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

  • API:/inApps/v1/transactions/consumption/{original_transaction_id}
  • 调⽤结果如下图示: 1c7082e3c88f48909167bc9e2a67c6c8~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

需要注意以下⼏点:

  • 由于交互过程中涉及到将在某应⽤的⽤户信息传输⾄苹果侧,所以需要在⽤户隐私协议中予以声明

  • 根据我们调研,⽬前该接⼝仍存在⼀个时间差问题:如果我们通知苹果,此时⽤户尚未消耗游戏币。但请求刚发出后,⽤户⼜对游戏币进⾏消耗了,那么相当于我们给苹果的信息已经是”过期的”。假设苹果由于我们反馈游戏币尚未消耗,所以同意退款,会导致仍旧出现坏账问题

关于这个问题,业务⽬前可以考虑在收到 CONSUMPTION_REQUEST 通知时,对账户进⾏冻结处理(48⼩时后调⽤ Refunded purchases API 查询订单状态,若未退款,予以解冻)

参考:

https://juejin.cn/post/6974733392260644895#heading-21 【WWDC21 10175】IAP 用户退款与客诉处理优化 - 小专栏.pdf