PAYATHON 2026

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);
    }
}

推荐按以下流程处理:

  1. Android 客户端向自己的服务端请求创建订单。
  2. 服务端生成 PhonePe payload 和 checksum。
  3. 客户端使用服务端返回的数据启动 PhonePe SDK。
  4. 用户完成或取消支付后,客户端返回应用。
  5. 服务端接收 callback,并验证回调内容。
  6. 服务端主动调用 PhonePe 状态查询接口。
  7. 只有查询结果明确为成功,才能将本地订单标记为已支付。
  8. 如果状态为 PENDING,应延迟后重新查询,不能立即将其视为失败。

callbackUrl 必须是公网可访问的 HTTPS 地址。临时 webhook 地址可以用来观察测试请求,但正式业务必须使用自己的服务端,并完成验签、幂等处理和订单金额核对。

最可能导致当前 400 的项目

建议依次检查:

  1. 将 R.string.merchantid 改为 getString(R.string.merchantid)。
  2. 将 ₹2 写成 amount = 200。
  3. 使用有效的 10 位测试手机号。
  4. 将 Base64 编码改为 Base64.NO_WRAP。
  5. 确认 checksum 使用的 endpoint 与 .setUrl(apiEndPoint) 完全一致。
  6. 确认 Merchant ID、Salt Key、Salt Index 和 endpoint 均属于同一环境。
  7. 输出编码前的 JSON,检查各字段的类型,但不要在日志中输出 Salt Key。
  8. 查看 PhonePe 返回的完整错误体或商户后台日志,确认具体错误码。

这段代码中最明确的请求问题,是 merchantId 被序列化成整数、金额单位错误以及 Base64 换行。解决 400 后,SDK 才会继续解析支付方式并尝试启动目标 UPI App。

备注:内容仅供参考。