H Honor
ZH
DiDi finanzas

文档中心

提供接入指南、接口文档、测试指引等全流程接入所需信息

2.6 Payment Status Webhook

1. Webhook 技术解决方案

BNPL api docs图 version 1_中文版-part2 - Webhook Tech Solution.jpg

当机构创建支付订单或退款订单时,消息将发送到滴滴BNPL核心系统。
滴滴BNPL Webhook系统将Webhook事件传递给机构(or 合作伙伴)系统。

2. Webhook 事件列表

事件类型 事件名称 事件描述
Payment PAYMENT.ORDER.PAID 支付成功
PAYMENT.ORDER.CANCELED 支付取消
PAYMENT.ORDER.DENIED 支付拒绝
PAYMENT.ORDER.EXPIRED 支付过期
当订单未支付时,该订单过期。
过期时间参考request中的expireTime字段,由Partner传入。
如果partner未传入该值,则默认为24hours。
Refund REFUND.ORDER.SUCCESS 退款成功
REFUND.ORDER.FAILED 退款失败

3 Webhook 事件消息格式

3.1 Payment Webhook

Header

字段名 字段描述 参数值 展示 (必填​/选填/条件性) 备注
Content-Type 内容类型 application/json 必填
X-Event-Timestamp 时间戳 e.g., 1733619128 必填 时间戳信息。
时间戳 (以秒为单位)。用于签名计算。
X-Event-Signature 签名 必填 签名信息。合作伙伴应验证此值。
X-Event-ID 唯一事件ID 必填 唯一事件id
X-Event-Topic 活动名称 必填 事件名称。您可以参考 "3.2 Webhook 事件列表".
X-Event-Delivery-ID 交付ID (UUID) e.g., "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" 必填 此webhook尝试的唯一传递ID (UUID)。
此字段不会在签名中计算。



Body

字段名 类型 字段描述 展示 (必填​/选填/条件性) 备注
partnerOrderId String 商家端支付单据ID 必填 幂等绑定
商户支付序列号ID
partnerOrderAmount Long 订单金额 必填 实际支付金额乘以100;该金额为已应用商家折扣后的最终金额。
单位是美分。例如:对于99.99比索,数值应为9999。
请使用与墨西哥比索货币相对应的金额。
currency String 货币 必填 ISO 4217标准-墨西哥比索MXN
expireTime String 到期时间 选填 模式:yyy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移 是必填项。
finishTime String 完成时间 选填 模式:yyyy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移是必填项。
createTime String 订单创建时间 选填 模式:yy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移是必填项。
updateTime String 订单状态已更新为“延迟” 选填 模式:yy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移是必填项。
rejectCode String 拒绝代码 选填 Value Existed in DENIED State.
仅当 X-Event-Topic 为 PAYMENT.ORDER.DENIED 时存在;其他事件类型(如 EXPIRED, CANCELED)该字段不存在或为空字符串。”
值处于已拒绝状态。仅当X-Event-Topic为PAYMENT.ORDER.DENIED时存在;其他事件类型EXPIRED,CANCELED该字段不存在或为空字符串。
rejectMsg String 拒绝理由 选填 值处于已拒绝状态。
test Boolean 测试订单备注标志。 选填 测试是指测试事务。



例子

{
    "partnerOrderId": "202512XXXX031300011032631762517",
    "partnerOrderAmount": 52231,
    "currency": "MXN",
    "expireTime": "2025-12-09T01:11:33.000+08:00",
    "finishTime": "2025-12-08T01:12:08.000+08:00",
    "createTime": "2025-12-08T01:11:33.000+08:00",
    "updateTime": "2025-12-08T01:12:08.000+08:00",
    "rejectCode": "",
    "rejectMsg": "",
    "test": true
}

3.2 Refund Webhook

Header

字段名 字段描述 参数值 展示 (必填​/选填/条件性) 备注
Content-Type 内容类型 application/json 必填
X-Event-Timestamp 时间戳 e.g., 1733619128 必填 时间戳信息。
时间戳 (以秒为单位)。用于签名计算。
X-Event-Signature 签名 必填 签名信息。合作伙伴应验证此值。
X-Event-ID 唯一事件ID 必填 唯一事件id
X-Event-Topic 活动名称 必填 事件名称。您可以参考 "3.2 Webhook 事件列表".
X-Event-Delivery-ID 交付ID (UUID) e.g., "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" 必填 此webhook尝试的唯一传递ID (UUID)。
此字段不会在签名中计算。



Body

字段名 类型 字段描述 展示 (必填​/选填/条件性) 备注
partnerOrderId String 商家端支付单据ID 必填 幂等绑定
商户支付序列号ID
partnerRefundId String 商户侧退款单据ID 必填 幂等绑定
refundAmount Long 退款金额 必填 实际退款金额金额单位为美分。例如,如果是99.99比索,这里的数值应为9999。
退款金额 = ∑(退款单价 × 退款数量)
currency String 币种 必填 ISO 4217标准-墨西哥比索MXN
rejectCode String 拒绝码 选填 值处于已拒绝状态
rejectMsg String 拒绝原因 选填 值处于已拒绝状态
refundTime String 退款时间 必填 模式: yyy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移是 必填项。
createTime String 订单创建时间 选填 模式: yyy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移是 必填项。
updateTime String 订单状态最新更新时间 选填 模式: yyy-MM-dd'T'HH:mm:ss.SSSXXX
例如,“2025-12-08T01:11:33.000+08:00"。时区偏移是 必填项。
test Boolean 测试订单备注标识。 选填 测试是指测试事务。



例子

{
    "partnerOrderId": "16000227_20260XXXX796022901278864",
    "partnerRefundId": "16000227_KBOtXXXX20260127202943864",
    "refundAmount": 9999,
    "currency": "MXN",
    "rejectCode": "",
    "rejectMsg": "",
    "refundTime": "2026-01-27T20:29:43.000+08:00",
    "createTime": "2026-01-27T20:29:43.000+08:00",
    "updateTime": "2026-01-27T20:29:48.000+08:00",
    "test": true
}

4. PAYMENT.ORDER.DENIED 的拒绝码列表

本列表中的 reject_code 仅在 PAYMENT.ORDER.DENIED 事件中返回,用于指示支付失败的具体原因。PAYMENT.ORDER.EXPIRED、PAYMENT.ORDER.CANCELED 等事件不返回此字段。

  • rejectCode
reject_code
TradePayFail
SkuBlackList
RiskRuleRejected
RiskControlRejected
PayRiskReject
PaylaterRuleRejected
OverdueRepayment
LoggedOut
GeneralError
ExitClose
CreditReject
CreditNotEnough
CreditActiveReject
CategoryBlackList

5. Webhook URL & Webhook 验签

5.1 Webhook URL

请将您的生产和测试环境的Webhook URL提供给DiDi员工进行配置,随后DiDi会将Webhook对应的密钥发送给您。

5.2 Webhook 验签

关于返回的Webhook,其Header中包含了签名,需要您开发Webhook消息的签名验证功能。


你可以参考下面的代码。

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;

/**
 * HMAC-SHA512 验签工具类
 * 格式:eventId:timestamp:data
 * 签名结果:16进制字符串
 */
public class HMACSHA512HexSignerVerify {

    private static final String HMAC_SHA512 = "HmacSHA512";
    private static final String DELIMITER = ":";

    /**
     * 验证签名
     *
     * @param eventId       事件ID, 取值说明: 请求header中的X-Event-ID
     * @param data          数据, 取值说明: 请求的完整响应
     * @param signature     待验证的签名(16进制字符串), 取值说明: 请求header中的X-Event-Signature
     * @param secretKey     密钥, 取值说明: 租户添加订阅时生成的唯一签名秘钥
     * @param timestamp     时间戳(秒), 取header中的X-Event-Timestamp
     * @param maxAgeSeconds 签名最大有效期(秒),0表示不验证有效期
     * @return 验证结果
     */
    public static boolean verifySignature(
            String eventId,
            String data,
            String signature,
            String secretKey,
            long timestamp,
            long maxAgeSeconds) {

        try {
            // 验证时间戳有效期
            if (maxAgeSeconds > 0) {
                long currentTime = Instant.now().getEpochSecond();
                if (Math.abs(currentTime - timestamp) > maxAgeSeconds) {
                    return false; // 签名已过期
                }
            }

            // 重新计算签名
            String expectedSignature = generateSignature(eventId, data, secretKey, timestamp);

            // 安全比较签名
            return constantTimeEquals(expectedSignature.toLowerCase(), signature.toLowerCase());

        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 生成签名(16进制字符串,带时间戳)
     *
     * @param eventId   事件ID
     * @param data      数据
     * @param secretKey 密钥
     * @param timestamp 时间戳(秒)
     * @return 16进制字符串签名
     */
    public static String generateSignature(String eventId, String data, String secretKey, long timestamp) {
        try {
            // 构建payload
            String payload = eventId + DELIMITER + timestamp + DELIMITER + data;

            // 计算HMAC-SHA512
            Mac mac = Mac.getInstance(HMAC_SHA512);
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                    secretKey.getBytes(StandardCharsets.UTF_8),
                    HMAC_SHA512
            );
            mac.init(secretKeySpec);
            byte[] hashBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));

            // 转换为16进制字符串
            return bytesToHex(hashBytes);

        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("生成签名失败", e);
        }
    }

    /**
     * 字节数组转16进制字符串
     *
     * @param bytes 字节数组
     * @return 16进制字符串
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }

    /**
     * 安全的字符串比较(防止时序攻击)
     *
     * @param str1 字符串1
     * @param str2 字符串2
     * @return 是否相等
     */
    private static boolean constantTimeEquals(String str1, String str2) {
        if (str1 == null || str2 == null) {
            return false;
        }

        if (str1.length() != str2.length()) {
            return false;
        }

        int result = 0;
        for (int i = 0; i < str1.length(); i++) {
            result |= str1.charAt(i) ^ str2.charAt(i);
        }
        return result == 0;
    }
    
}

6 Webhook 投递与重试规则

最大投递次数:共计10次(含首次投递,失败后自动重试9次)
重试触发条件:机构(or 合作伙伴)系统收到Webhook事件时返回的 http code 没有在 [200, 300) 区间。
重试策略:采用底数为2的指数退避重试策略
重试时间规则:设首次投递失败后的首次重试时间为t,后续每一轮重试队列的触发时间依次为:t、t+2、t+4、t+8、t+16、t+32、t+64、t+256、t+512、t+1024 (单位:秒)

复制成功