PAYATHON 2026

PhonePe Android SDK 返回 401 错误如何解决?

支付小周

结论

ERROR_B2B_API_RETURNED_ERROR 和 {"success":false,"code":"401"} 表明 PhonePe Android SDK 调用支付网关时未通过上游接口的身份验证。问题通常不在 onActivityResult 的处理逻辑,而在支付请求的商户凭证、签名(checksum / X-VERIFY)或运行环境。

建议先检查:

  • merchantId 是否正确;
  • saltKey、saltIndex 是否属于当前商户;
  • 测试环境和生产环境的凭证、SDK 配置及接口地址是否混用;
  • 签名所用的请求体和 API 路径是否与实际发送的内容完全一致;
  • Base64 字符串、JSON 或请求体是否在签名后又被修改。

为什么会出现 401

HTTP 401 表示请求未通过认证。PhonePe SDK 返回的 ERROR_B2B_API_RETURNED_ERROR 只是对网关错误的封装,排查时应重点关注网关返回的 401。

常见原因如下。

商户凭证不正确

merchantId、saltKey 或 saltIndex 中任何一项有误,都可能导致认证失败。除了检查 SDK 初始化时使用的 merchantId,还要确认支付请求体中的 merchantId 与它一致。

测试环境和生产环境混用

测试环境和生产环境通常使用不同的商户凭证与网关配置。例如:

  • 使用测试凭证初始化生产环境;
  • 使用生产凭证调用测试环境;
  • SDK 环境配置正确,但后端使用了另一套配置生成签名。

环境、凭证和网关配置必须配套使用。

X-VERIFY 或 checksum 计算错误

对于需要 checksum 的 PhonePe 接口,签名通常由以下内容计算:

Base64 encoded payload + API path + saltKey

计算 SHA-256 后,再拼接 saltIndex:

SHA256(base64Payload + apiPath + saltKey) + "###" + saltIndex

参与签名的字段和路径应以当前接入版本的官方文档为准。特别要检查签名中的 apiPath,它必须与实际请求使用的路径完全一致。

签名内容与发送内容不一致

以下操作都会导致签名失效:

  • 签名完成后重新序列化 JSON;
  • 再次编码 Base64 字符串;
  • 使用一份请求体生成签名,却让 SDK 发送另一份请求体;
  • 增加、删除或修改请求字段;
  • 使用带换行的 Base64 编码,导致请求体中出现不可见的换行符;
  • 签名时使用完整 URL,而接口要求使用 URI path,或情况正好相反。

建议的排查步骤

1. 核对当前环境

确认 SDK 初始化环境、支付接口地址和商户凭证属于同一环境。不要只看变量名称,要核对程序实际加载的配置值。

日志中可以记录以下非敏感信息:

environment
merchantId
saltIndex
API path
merchantTransactionId

不要记录 saltKey、完整签名或其他敏感凭证。

2. 检查凭证来源

确认 merchantId、saltKey 和 saltIndex 来自 PhonePe 为当前支付网关商户提供的配置,而不是其他产品、其他商户或另一环境的凭证。

如果配置由团队其他成员或服务端提供,请让拥有 PhonePe 商户后台权限的人员重新核对。

3. 在服务端重新计算签名

如果 Android 客户端正在保存 saltKey 并生成 checksum,应将这部分逻辑移到可信服务端。把 saltKey 放在 APK 中有泄露风险,攻击者可能通过反编译获取它。

下面的示例只演示常见的 SHA-256 计算方式。apiPath 和具体签名规则仍须按照当前 PhonePe 接口文档确认:

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

public final class PhonePeChecksum {

    public static String generate(
            String requestJson,
            String apiPath,
            String saltKey,
            String saltIndex
    ) throws Exception {
        String base64Payload = Base64.getEncoder()
                .encodeToString(requestJson.getBytes(StandardCharsets.UTF_8));

        String valueToHash = base64Payload + apiPath + saltKey;

        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(valueToHash.getBytes(StandardCharsets.UTF_8));

        StringBuilder hex = new StringBuilder();
        for (byte value : hash) {
            hex.append(String.format("%02x", value & 0xff));
        }

        return hex + "###" + saltIndex;
    }
}

传给 SDK 或网关的 base64Payload 必须与参与签名的字符串完全相同。

4. 比较实际请求与签名输入

在不记录敏感信息的情况下,逐项检查:

签名使用的 Base64 payload == 实际发送的 Base64 payload
签名使用的 API path == 实际请求的 API path
请求体 merchantId == 当前凭证对应的 merchantId
checksum 后缀 saltIndex == 当前 saltKey 对应的 saltIndex

可以分别记录 payload 的 SHA-256 摘要进行比对,无需在生产日志中打印完整的支付请求。

5. 检查 onActivityResult 的职责

onActivityResult 收到结果,只能说明 SDK 流程返回了错误,不能据此确认支付最终成功还是失败。解决认证问题后,仍应由服务端调用 PhonePe 的订单状态查询接口确认最终状态,不要只根据客户端回调更新订单。

示意代码如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == PHONEPE_REQUEST_CODE) {
        // 客户端回调只用于触发后续处理。
        // 最终支付状态应由服务端通过状态查询接口确认。
        queryPaymentStatusFromYourServer();
    }
}

如果配置无误仍然返回 401

准备以下信息,联系 PhonePe 支持或商户接入负责人:

  • 商户 ID;
  • 测试或生产环境;
  • merchantTransactionId;
  • 请求发生的时间和时区;
  • SDK 返回的错误码;
  • 网关响应中的 request ID、trace ID 或 transaction ID(如果有);
  • 已脱敏的请求体;
  • apiPath、saltIndex 和签名算法说明。

不要通过工单、聊天记录或公开代码仓库发送 saltKey。

如果同一套凭证以前可以正常使用,代码也没有发生变化,还应请 PhonePe 确认商户是否已启用对应环境、凭证是否经过轮换,以及相关支付能力是否已开通。仅凭 {"code":"401"} 无法判断具体是哪项配置出错,仍需结合服务端原始响应和 PhonePe 侧日志定位。

备注:内容仅供参考。