Android 集成 PhonePe 支付网关时出现 400 错误
结论
400 表示 PhonePe 在发起支付前拒绝了请求。问题通常出在请求体、checksum 或环境参数上,而不是 UPI App 无法启动。
这段代码至少有以下问题:
data.put("merchantId", R.string.merchantid)传入的是 Android 资源 ID,也就是一个整数,并非 Merchant ID 字符串。amount = 2表示 2 paise,不是 ₹2,而且很可能低于网关允许的最低金额。mobileNumber = "123456789"只有 9 位,不符合印度手机号码格式。Base64.DEFAULT可能插入换行符,导致请求数据与 checksum 不一致,应改用Base64.NO_WRAP。merchantUserId所在行多了一个右括号。如果不是粘贴时产生的错误,这段代码无法通过编译。targetApp固定为com.phonepe.app,只会尝试打开 PhonePe,不会列出所有 UPI App。- UAT 和生产环境的 Merchant ID、Salt Key、Salt Index、API endpoint 必须相互匹配。
- 将 Salt Key 放在 Android 客户端并不安全。正式集成时,请求和 checksum 应由服务端生成。
- 从支付 App 返回 Activity 不代表支付成功。最终状态必须由服务端调用 PhonePe 状态接口确认。
首先修正请求参数
在 Manifest 中引用字符串资源没有问题:
<meta-data
android:name="com.phonepe.android.sdk.MerchantId"
android:value="@string/merchantid" />
但 Java 代码中的 R.string.merchantid 是整数资源编号,需要通过 getString() 取得实际字符串:
String merchantId = getString(R.string.merchantid);
data.put("merchantTransactionId", getTransactionId);
data.put("merchantId", merchantId);
data.put("merchantUserId", "ABC12345");
data.put("amount", 200); // 单位为 paise,即 ₹2.00
data.put("mobileNumber", "9876543210");
data.put("callbackUrl", "https://your-domain.example/phonepe/callback");
data.put("redirectMode", "POST");
data.put("redirectUrl", "https://your-domain.example/payment/redirect");
amount 通常使用整数 paise:
₹1.00 = 100
₹2.00 = 200
₹10.50 = 1050
最低金额应以当前 PhonePe 商户后台和所用 API 版本的要求为准。
正确生成 Base64 和 checksum
编码时应明确指定 UTF-8,并禁止 Base64 自动换行:
import android.util.Base64;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
String json = gson.toJson(data);
String base64Payload = Base64.encodeToString(
json.getBytes(StandardCharsets.UTF_8),
Base64.NO_WRAP
);
String checksumInput = base64Payload + apiEndPoint + saltKey;
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(
checksumInput.getBytes(StandardCharsets.UTF_8)
);
StringBuilder hex = new StringBuilder();
for (byte value : hash) {
hex.append(String.format("%02x", value & 0xff));
}
String checksum = hex + "###" + saltIndex;
apiEndPoint 是最容易出错的部分。参与 checksum 计算的路径必须与 SDK 请求使用的路径完全一致,需要逐项检查:
- 是否包含版本路径;
- 是否包含开头的
/; - 大小写是否一致;
- 是否误用了完整域名;
- 结尾是否多了或少了
/。
不要对 JSON 或 Base64 字符串进行二次编码,也不要在生成 checksum 后修改 payload。
构造并启动支付请求
B2BPGRequest request = new B2BPGRequestBuilder()
.setData(base64Payload)
.setChecksum(checksum)
.setUrl(apiEndPoint)
.build();
try {
Intent intent = PhonePe.getImplicitIntent(
this,
request,
"com.phonepe.app"
);
if (intent != null) {
startActivityForResult(intent, B2B_PG_REQUEST_CODE);
} else {
// PhonePe App 未安装,或没有可处理该 Intent 的应用
}
} catch (PhonePeInitException e) {
Log.e("PhonePe", "PhonePe SDK initialization failed", e);
}
如果需要让用户选择其他受支持的 UPI App,就不能始终固定为:
"com.phonepe.app"
应根据当前 SDK 文档传入目标 App 的包名,或使用 SDK 提供的通用 Intent、App 列表功能。不同 SDK 版本的方法签名可能不同,不能直接假定传入空值一定有效。
检查 UAT 配置
给出的 Manifest 配置是旧版 SDK 使用 UAT 或模拟环境时的写法:
<meta-data
android:name="com.phonepe.android.sdk.Debuggable"
android:value="true" />
<meta-data
android:name="com.phonepe.android.sdk.isUAT"
android:value="true" />
<meta-data
android:name="com.phonepe.android.sdk.isSimulator"
android:value="true" />
检查配置时需要注意:
isUAT=true时只能使用 UAT 凭据和 UAT endpoint。- 切换到生产环境时,需要同时更换 Merchant ID、Salt Key、Salt Index、endpoint 和相关环境开关。
isSimulator=true只适用于对应的模拟测试流程。真机联调时是否需要关闭,应以当前 SDK 版本和 PhonePe 提供的测试方案为准。- Merchant ID 前后不能有空格。
- UAT Merchant ID 不能与生产环境的 Salt Key 混用。
问题中引用的是 /v1/ 文档。如果项目使用了较新的 PhonePe SDK,还需要确认该版本是否继续支持这些 Manifest 键和 B2BPGRequest 流程。
正确处理支付结果
onActivityResult() 只能说明用户已经从 UPI App 返回,不能证明支付成功。用户可能取消了支付,交易也可能仍在处理中,客户端返回的数据还可能被篡改。
可以在回调中通知服务端查询订单状态:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == B2B_PG_REQUEST_CODE) {
// 不要仅凭 resultCode 判定付款成功。
// 将 merchantTransactionId 发给自己的服务端,
// 由服务端调用 PhonePe Order Status API 查询最终状态。
checkPaymentStatusFromServer(getTransactionId);
}
}
推荐按以下流程处理:
- Android 客户端向自己的服务端请求创建订单。
- 服务端生成 PhonePe payload 和 checksum。
- 客户端使用服务端返回的数据启动 PhonePe SDK。
- 用户完成或取消支付后,客户端返回应用。
- 服务端接收 callback,并验证回调内容。
- 服务端主动调用 PhonePe 状态查询接口。
- 只有查询结果明确为成功,才能将本地订单标记为已支付。
- 如果状态为
PENDING,应延迟后重新查询,不能立即将其视为失败。
callbackUrl 必须是公网可访问的 HTTPS 地址。临时 webhook 地址可以用来观察测试请求,但正式业务必须使用自己的服务端,并完成验签、幂等处理和订单金额核对。
最可能导致当前 400 的项目
建议依次检查:
- 将
R.string.merchantid改为getString(R.string.merchantid)。 - 将 ₹2 写成
amount = 200。 - 使用有效的 10 位测试手机号。
- 将 Base64 编码改为
Base64.NO_WRAP。 - 确认 checksum 使用的 endpoint 与
.setUrl(apiEndPoint)完全一致。 - 确认 Merchant ID、Salt Key、Salt Index 和 endpoint 均属于同一环境。
- 输出编码前的 JSON,检查各字段的类型,但不要在日志中输出 Salt Key。
- 查看 PhonePe 返回的完整错误体或商户后台日志,确认具体错误码。
这段代码中最明确的请求问题,是 merchantId 被序列化成整数、金额单位错误以及 Base64 换行。解决 400 后,SDK 才会继续解析支付方式并尝试启动目标 UPI App。
备注:内容仅供参考。
