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 侧日志定位。
备注:内容仅供参考。