PAYATHON 2026

退款接口调用成功是否代表退款完成?是否支持异步通知?

支付小周

结论

退款接口调用成功,通常只表示系统已受理退款请求,不能直接说明退款已经完成。最终结果应以退款查询接口返回的状态、支付平台账单结果,或业务系统确认的退款状态为准。

是否支持退款异步通知,要看具体支付平台和接口版本。如果退款接口没有提供“异步通知地址”参数,平台可能使用商户后台预先配置的统一通知地址,也可能要求业务系统主动轮询退款查询接口。仅凭接口入参中没有通知地址,不能判断平台一定不会发送异步通知。

为什么接口成功不等于退款完成

退款处理一般会经过几个阶段:

  1. 业务系统提交退款请求。
  2. 支付平台校验订单、退款金额和权限。
  3. 平台受理退款请求并返回结果。
  4. 平台向银行、渠道或清算机构发起实际退款。
  5. 渠道返回最终处理结果。
  6. 业务系统更新订单和退款单状态。

接口响应中的“成功”,可能表示:

  • 请求参数校验通过;
  • 退款单已创建;
  • 平台已受理退款;
  • 退款已进入处理中。

只有当响应明确表示退款状态为“成功完成”,且该状态符合接口文档定义时,才能据此确认退款完成。如果返回“处理中”“受理成功”或“等待渠道处理”等状态,还需要继续查询最终结果。

如何确认退款最终结果

先保存退款请求信息

调用退款接口时,应保存以下信息:

  • 商户退款单号;
  • 原支付订单号;
  • 退款金额;
  • 平台返回的退款单号;
  • 接口返回状态和错误信息;
  • 请求时间及响应原文(注意脱敏保存)。

退款单号是后续查询、对账和排查问题时的重要依据。

查询退款状态

如果平台提供退款查询接口,应使用退款单号或原支付订单号查询状态。常见状态可能包括:

  • SUCCESS:退款成功;
  • PROCESSING:退款处理中;
  • FAILED:退款失败;
  • CLOSED:退款关闭。

具体状态名称和含义必须以对应平台的接口文档为准,不能直接套用其他支付平台的枚举值。

示例代码如下,字段名仅用于说明调用流程,实际参数请替换为平台文档要求的内容:

import time
import requests

def query_refund(refund_no):
    response = requests.post(
        "https://api.example.com/refund/query",
        json={"refund_no": refund_no},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

refund_no = "REFUND_202609270001"

for _ in range(10):
    result = query_refund(refund_no)
    status = result.get("status")

    if status == "SUCCESS":
        print("退款已完成")
        break

    if status in ("FAILED", "CLOSED"):
        print("退款未完成:", status)
        break

    time.sleep(10)
else:
    print("暂未获取到最终结果,请继续通过异步通知或定时任务确认")

轮询次数和间隔应结合平台限流要求、退款时效和业务容忍度设置,避免高频、无限制地调用接口。

没有通知地址参数时,如何接收退款通知

检查平台配置项

有些平台不会在每次退款请求中传入通知地址,而是在商户后台或应用配置中设置统一的退款通知地址。需要确认:

  • 商户后台是否配置了退款结果通知地址;
  • 退款通知是单独配置,还是复用支付结果通知地址;
  • 当前应用、商户号、支付产品和环境是否使用同一套配置;
  • 通知地址是否必须使用 HTTPS;
  • 平台是否要求公网可访问,是否支持特定端口或域名。

具体配置应以平台文档和商户后台显示的内容为准。

使用平台提供的统一回调接口

如果平台支持退款异步通知,通常会向预先配置的地址发送 HTTP 请求。业务系统应完成以下处理:

  1. 接收平台通知。
  2. 验证签名、证书或其他身份凭证。
  3. 校验通知中的退款单号、订单号和金额。
  4. 根据退款状态更新本地退款单。
  5. 对重复通知进行幂等处理。
  6. 按平台要求返回成功响应。

示例接口结构:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/callbacks/refund")
def refund_callback():
    payload = request.get_json(silent=True) or {}

    # 1. 根据平台文档验证签名
    # verify_signature(request.headers, payload)

    refund_no = payload.get("refund_no")
    status = payload.get("status")
    amount = payload.get("amount")

    if not refund_no or not status:
        return jsonify({"code": "INVALID_PARAMETER"}), 400

    # 2. 查询本地退款单
    refund_order = find_refund_order(refund_no)

    # 3. 校验订单、金额,避免错误通知更新业务数据
    if refund_order is None:
        return jsonify({"code": "REFUND_NOT_FOUND"}), 404

    if str(amount) != str(refund_order.amount):
        return jsonify({"code": "AMOUNT_MISMATCH"}), 400

    # 4. 幂等更新:已成功的退款不要重复处理
    if refund_order.status != "SUCCESS":
        update_refund_status(refund_no, status)

    # 5. 按平台要求返回确认结果
    return jsonify({"code": "SUCCESS"}), 200

上面的函数名和响应格式只是示例,不能直接视为某个平台的固定协议。签名校验方式、通知字段、响应内容和 HTTP 状态码,都应严格按照实际接口文档实现。

没有异步通知能力时,主动查询

如果平台既没有退款通知配置,也没有异步通知接口,就需要主动查询:

  • 退款请求成功后立即查询一次;
  • 如果处于处理中,使用定时任务继续查询;
  • 超过合理时间仍未得到结果时,转入人工或异常处理队列;
  • 定期将本地退款记录与平台账单进行对账。

主动查询不能只在接口调用失败时执行。接口调用成功但返回处理中时,同样需要继续确认最终状态。

处理接口响应时的建议

业务代码最好区分“请求是否受理”和“退款是否完成”两个状态。例如:

request_accepted = true
refund_status = PROCESSING

不要简单地只使用一个布尔值:

refund_success = true

如果出现网络超时、连接中断或未知响应,也不要立即再次发起全新的退款请求。应先使用原退款单号查询结果,避免重复退款。同一订单是否允许多次退款、同一退款单是否可以重试,也必须遵循平台的幂等规则。

注意事项

  • 以退款查询结果或已验证的异步通知作为最终状态依据。
  • 不要把 HTTP 200、接口返回码成功或“受理成功”直接解释为退款已到账。
  • 异步通知可能重复、延迟或乱序,必须实现幂等处理。
  • 更新退款状态前,应校验订单号、退款单号、金额和签名。
  • 对“处理中”状态设置合理的超时和异常处理机制。
  • 通知接口应快速返回,耗时业务可以放入队列异步处理。
  • 保存请求和通知日志时,应对身份证号、银行卡号、签名等敏感信息脱敏。
  • 如果接口文档没有明确说明异步通知规则,不要自行推断,应向支付平台确认退款结果通知配置、通知字段、重试机制和最终状态定义。

备注:内容仅供参考。