签约成功,扣款被拒:支付宝周期扣款(免密代扣)的传参约束与 ACQ.* 归因
用户在支付宝 H5 页面上走完签约流程,agreement_no 也拿到了,第一次用 alipay.trade.pay 发起扣款却返回 40004 / ACQ.AGREEMENT_ERROR。这条链路上接口本身不长,绝大多数失败都集中在两处:签约时 period_rule_params 定下的周期约束,以及扣款时 auth_code 到底传了什么。
适用范围:支付宝钱包 H5 场景下的周期/商家扣款,签约产品码 CYCLE_PAY_AUTH_P(支付并签约场景用 CYCLE_PAY_AUTH)。如果不涉及周期计划、只是单次免密支付,本文的周期约束部分不适用。
一、签约侧:alipay.user.agreement.page.sign
接口名 alipay.user.agreement.page.sign,支付宝个人协议页面签约接口,用在支付宝钱包 H5 页面场景。
必传参数:
personal_product_code(必选,64 位)access_params(必选)period_rule_params(特殊可选:签约周期扣款产品如CYCLE_PAY_AUTH_P时必传,签约其他产品无需传)
一对多模式下 external_agreement_no 与 sign_scene 的配对
external_agreement_no(外部商户签约号)与 sign_scene(协议签约场景)有配对规则:设置了 external_agreement_no,sign_scene 必传;只设置 sign_scene,external_agreement_no 选传。sign_scene 设错会直接报「开通失败 / 签约失败」。
period_rule_params 的周期管控规则
period_type=DAY时,period最少为 7 天,execute_time可设置任意一天。period_type=MONTH时,period表示几月为一周期,execute_time需在 28 日之前,29–31 不可设置(月份可能不存在)。- 周期扣款产品必须传入
period_rule_params,其中period_type、period、execute_time、single_amount都要填。 execute_time:首次执行时间,周期扣款产品必填,即商户发起首次扣款的时间,精确到日,格式yyyy-MM-dd。结合其他必填的扣款周期参数,会确定商户以后的扣款计划;发起扣款的时间需符合这个计划。- 官方对
period_rule_params的描述是:周期扣款产品会按这里传入的参数提示用户,并对发起扣款的时间、金额、次数做相应限制。
支付后签约:参数放 agreement_sign_params
支付后签约场景,签约业务参数放在 agreement_sign_params 里。常见的「支付成功但签约未成功」不是支付回调没收到,而是签约业务参数配错。逐项检查:product_code 是否改成 CYCLE_PAY_AUTH;agreement_sign_params 里是否配置了签约业务参数;personal_product_code、access_params、period_rule_params 是否齐备;period_rule_params 规则是否正确;一对多模式下 external_agreement_no 与 sign_scene 是否同时存在;用户在支付签约页面是否开通了周期/商家扣款协议按钮。
稳妥的顺序是先调 alipay.user.agreement.page.sign 能签约成功,再把签约业务参数搬进 agreement_sign_params。
签约失败同步跳转回带的报错原文形如:
https://同步地址?msg=Business Failed&charset=utf-8&code=40004&method=alipay.user.agreement.page.sign.return&sub_msg=系统异常,签约失败,商户请求数据错误&sub_code=INVALID_PARAMETER&sign=***&external_agreement_no=202006199000000000&version=1.0&auth_app_id=2019*********42&personal_product_code=CYCLE_PAY_AUTH_P&app_id=2019*********42&sign_type=RSA2&sign_scene=INDUSTRY|CARRENTAL×tamp=2020-06-19 15:24:10
无线端唤起签约
把 alipay.user.agreement.page.sign 请求生成的数据重新转义拼接,在商家 App 端唤起支付宝客户端:
alipays://platformapi/startapp?appId=60000157&appClearTop=false&startMultApp=YES&sign_params=转义后的数据
拼接出的 alipay 协议短链数据固定不变,不要改动其中的 appId 等值。
排查按官方三步走:
- 先用
pageExecute()以 get 请求生成https://openapi.alipay.com/gateway.do开头的请求参数链接,浏览器或客户端直接访问。测试时先只传必传参数,避免选填参数干扰。 - 接口生成的参数能正常访问,说明参数正确,问题在拼接或唤起方式;把拼接好的数据放到钉钉聊天页直接点击,看能否唤起支付宝客户端。
- 前两步都正常,再查唤起方式:Android 判断 package name
com.eg.android.AlipayGphone是否存在;iOS 判断alipays://能否打开,iOS 9 以上要把alipays配置进LSApplicationQueriesSchemes,不要配到 URL Schemes。
签约结果通知
notify_type=dut_user_sign,携带 status(NORMAL)、agreement_no、external_agreement_no、sign_time、valid_time、invalid_time、single_quota、next_deduct_time、sign_scene、personal_product_code、alipay_user_id 等字段。通知示例(脱敏):
https://www.merchant.com/receive_notify.htm?notify_id=...¬ify_time=2017-02-16 21:46:15&sign_type=RSA2&sign=...&app_id=2017060101317939&external_agreement_no=test&merchant_app_id=2014072300007148&invalid_time=2017-05-20 11:49:19&sign_time=2017-05-20 11:49:19&alipay_user_id=...&status=NORMAL&personal_product_code=GENERAL_WITHHOLDING_P&valid_time=2017-05-20 11:49:19&agreement_no=20170502000610755993&sign_scene=INDUSTRY|CARRENTAL&external_logon_id=...¬ify_type=dut_user_sign&single_quota=100&next_deduct_time=2024-01-01
二、扣款侧:alipay.trade.pay
接口 alipay.trade.pay(统一收单交易支付接口,部分文档写作 alipay.trade.order.pay)。用户在商户侧已授权下单并享受服务后,商户用授权单号通过本接口对已授权金额发起扣款。
传参要点:
- 加
product_code,值必须是周期/商家扣款的固定值CYCLE_PAY_AUTH(对比当面付product_code=FACE_TO_FACE_PAYMENT)。 scene=deduct_pay。auth_code= 签约成功返回的agreement_no。- 核实
auth_code的值是否存在、是否填对。
ACQ.AGREEMENT_ERROR 的报错原文:
{"alipay_trade_pay_response":{"code":"40004","msg":"Business Failed","sub_code":"ACQ.AGREEMENT_ERROR","sub_msg":"协议信息异常","buyer_pay_amount":"0.00","invoice_amount":"0.00","point_amount":"0.00","receipt_amount":"0.00"},"sign":"****"}
原因有两类:接口并发;传入的 auth_code 不属于该 product_code 下的协议号。并发导致的报错不要高频重试,过几秒再试;协议传参错误就按上面几条核对 product_code / scene / auth_code。
三、ACQ.* 错误码逐条归因
ACQ.ACCESS_FORBIDDEN无权限使用接口:未签约对应产品合约。核对product_code是否正确,确认商户是否签约了对应产品合约。ACQ.AGREEMENT_ERROR协议信息异常:检查传入的协议信息是否正确。ACQ.AGREEMENT_NOT_EXIST/USER_AGREEMENT_NOT_EXIST协议信息不存在 / 用户协议不存在:代扣传入的协议号对应的用户协议不存在或已解约。已解约的需引导用户重新签约,用新生成的协议号发起扣款。AGREEMENT_INVALID用户协议失效。ACQ.AMOUNT_OR_CURRENCY_ERROR订单金额或币种信息错误:检查金额信息,或当前币种是否未签约。ACQ.AUTH_AMOUNT_NOT_ENOUGH授权金额不足:订单金额大于授权剩余金额。ACQ.AUTH_NO_ERROR预授权号错误或状态不对:确认auth_no正确、参与方一致、状态为已授权。ACQ.CYCLE_PAY_DATE_NOT_MATCH扣款日期不在签约时的允许范围之内:签约时约定了扣款周期,发起日期不符合约定周期则不允许扣款,重新检查扣款日期。ACQ.CYCLE_PAY_SINGLE_FEE_EXCEED周期扣款的单笔金额超过签约时限制。ACQ.CYCLE_PAY_TOTAL_FEE_EXCEED周期扣款的累计金额超过签约时限制。ACQ.CYCLE_PAY_TOTAL_TIMES_EXCEED周期扣款的总次数超过签约时限制。ACQ.MERCHANT_AGREEMENT_INVALID商户协议已失效:商户与支付宝的合同已失效。ACQ.OVER_DEDUCT_PERIOD超过扣款周期:已超过扣款周期时效无法发起新的扣款,请使用已经发起的扣款单号重试。ACQ.ZM_AUTH_AMOUNT_EXCEED先用后付场景下超过约定的免密支付金额:需商户调用支付宝 SDK 唤起收银台,用户确认后付款。
四、设计上的取舍
agreement_no是扣款链路的主键和幂等键。签约通知里拿到的要落库,扣款时传的是它,不是用户的 uid,也不是external_agreement_no(商户侧单号,用于一对多把同一用户的多份协议区分开)。- 扣款计划在签约那一刻就被
period_rule_params锁死了。period_type/period/execute_time决定以后能扣的日期,single_amount与签约限额决定单笔上限。上线后想改扣款日或提额,改代码没用,得让用户重新签约。 - 解约/失效是正常业务分支,不是异常。出现
ACQ.AGREEMENT_NOT_EXIST时不要再重试扣款,落库标记协议不可用,走站内信/短信引导重签。 ACQ.OVER_DEDUCT_PERIOD是特殊分支:超过扣款周期时效不能用新单号,要用已经发起的那个扣款单号重试,实现「新单号重试」反而会一直报错。- 签约和扣款都是异步的。签约结果以
dut_user_sign通知里的status为准,扣款结果以异步通知为准,不要拿接口返回的 200 当最终状态。 - 需要按自己申请的产品合约核对的项:
SCENE值枚举、各签约产品码的准入资质、扣款失败重试次数与频控策略。这几项在官方文档里分散在不同页面。