PAYATHON 2026

如何在 Android Studio 中集成 Phone Pay Card Payment

支付阿杰

结论

Card Payment 不能通过“把请求发送给某个 UPI App”来完成。UPI Intent 只处理 UPI 支付。银行卡支付需要进入 PhonePe Payment Gateway 的统一收银台,再由收银台提供 Card、UPI 等支付方式。

ERROR_B2B_API_RETURNED_ERROR 并不是具体的银行卡错误。它是 Android SDK 在“PhonePe 后端拒绝或未能成功处理交易请求”时返回的通用提示。仅凭 onActivityResult 无法判断根因,还需要结合交易创建接口的响应、merchantTransactionId、PhonePe 服务端状态查询结果和商户后台日志进行排查。

正确的支付流程

调用链应当是:

Android App
    ↓ 请求创建订单
商户服务端
    ↓ 使用 merchantId / saltKey 生成支付请求和校验值
PhonePe Payment Gateway
    ↓ 返回可供 SDK 使用的 transaction request
Android App
    ↓ 启动 PhonePe PG SDK
PhonePe 收银台
    ↓ 用户选择 Card / UPI 等方式
商户服务端
    ↓ 查询交易状态并验证最终结果
Android App 展示支付结果

如果现有代码直接构造 UPI Intent,跳转到 PhonePe、Google Pay 或其他 UPI App,这条链路只能完成 UPI 支付,无法切换到银行卡支付。

Card Payment 通常不需要 Android 端调用单独的“银行卡 App”。正确做法是:

  1. 接入 PhonePe PG SDK 或官方托管收银台。
  2. 在 PhonePe 商户配置中开通 Card 支付方式。
  3. 由商户服务端生成合法的支付请求。
  4. 通过 SDK 打开统一收银台。
  5. 支付返回后,由服务端查询并确认交易状态。

为什么会出现 ERROR_B2B_API_RETURNED_ERROR

这个错误说明 SDK 调用 PhonePe B2B/PG 后端时收到了失败响应。常见原因有:

  • 使用了 UPI Intent 请求,没有使用 PG Checkout 请求。
  • 商户账户尚未开通 Card Payment。
  • 混用了测试环境和生产环境的 merchantId、saltKey 或 saltIndex。
  • 计算签名时使用了错误的 API path。
  • Base64 编码内容与计算 X-VERIFY 时使用的内容不一致。
  • merchantTransactionId 重复、为空或格式不符合要求。
  • 金额单位错误。多数支付接口要求使用最小货币单位,例如 ₹1.00 应传 100,具体以当前 PhonePe 文档为准。
  • callbackUrl、redirectUrl 或移动端回跳配置无效。
  • SDK、服务端 API 和接入文档不属于同一版本。
  • 测试商户或测试卡不支持当前选择的支付方式。
  • 服务端已经返回了具体的业务错误,但 Android 端只收到 SDK 的统一错误码。

因此,看到 ERROR_B2B_API_RETURNED_ERROR 时,不能直接认定 SDK 出现故障或银行卡支付失败。

Android 端应如何调用

不同版本的 PhonePe SDK 可能使用不同的类名和参数。下面采用部分旧版 Android PG SDK 文档中常见的 B2BPGRequestBuilder 写法,只用于说明调用结构。实际项目应以当前商户后台提供的 SDK 版本和官方文档为准。

String base64Body = paymentResponse.getBase64Body();
String checksum = paymentResponse.getChecksum();
String apiEndPoint = paymentResponse.getApiEndPoint();

B2BPGRequest b2BPGRequest = new B2BPGRequestBuilder()
        .setData(base64Body)
        .setChecksum(checksum)
        .setUrl(apiEndPoint)
        .build();

Intent intent = PhonePe.getImplicitIntent(
        this,
        b2BPGRequest,
        ""
);

if (intent != null) {
    startActivityForResult(intent, PHONEPE_REQUEST_CODE);
} else {
    // 当前设备无法处理该 Intent。
    // 根据所用 SDK 的官方说明决定是否改用显式 Intent 或 Web Checkout。
}

base64Body 和 checksum 应由商户服务端生成并返回。不要在 Android App 中保存 saltKey,也不要直接在客户端生成签名。

如果使用新版 SDK,接入入口可能已经调整为新的 transaction API 或 Activity Result API。旧版示例中的请求对象、API path 和 checksum 算法不能直接套用到新版接口。

服务端生成请求的关键点

下面的代码只用于说明签名流程,不能作为固定接口定义直接复制。apiPath、请求字段和签名算法必须与当前 PhonePe 文档完全一致。

String requestJson = objectMapper.writeValueAsString(paymentRequest);

String base64Body = Base64.getEncoder()
        .encodeToString(requestJson.getBytes(StandardCharsets.UTF_8));

String apiPath = "/pg/v1/pay"; // 示例;以实际接入版本为准

String stringToSign = base64Body + apiPath + saltKey;
String sha256 = sha256Hex(stringToSign);

String xVerify = sha256 + "###" + saltIndex;

需要重点核对:

X-VERIFY = SHA256(base64Body + apiPath + saltKey) + "###" + saltIndex

即使 JSON 内容看起来相同,只要出现以下任一差异,校验就可能失败:

  • Android 端重新序列化了 JSON。
  • Base64 使用了换行编码。
  • 签名中的 API path 与实际请求地址不一致。
  • 使用了错误环境的 saltKey。
  • saltIndex 不匹配。
  • 完成签名后又修改了请求字段。

更稳妥的方式是由服务端一次性生成最终的 Base64 body 和 checksum,Android 端不做修改,直接传给 SDK。

不要只依赖 onActivityResult

旧版接入可能会收到类似结果:

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

    if (requestCode != PHONEPE_REQUEST_CODE) {
        return;
    }

    String errorCode = null;

    if (data != null && data.getExtras() != null) {
        for (String key : data.getExtras().keySet()) {
            Object value = data.getExtras().get(key);
            Log.d("PhonePeResult", key + " : " + value);

            if ("key_error_code".equals(key) && value != null) {
                errorCode = value.toString();
            }
        }
    }

    // 无论 Activity 返回什么,都应通知服务端查询交易状态。
    verifyTransactionOnServer(merchantTransactionId);
}

需要注意:

  • RESULT_OK 不代表资金一定已经到账。
  • RESULT_CANCELED 也不一定代表交易最终失败,因为支付状态可能仍是处理中。
  • 即使 Intent 中没有数据,也要通过服务端查询交易状态。
  • 最终结果应以 PhonePe 服务端状态查询和经过验证的回调为准。
  • 如果 SDK 支持,新项目应使用 Activity Result API。是否可用取决于具体的 SDK 版本。

建议的排查顺序

1. 确认当前使用的支付模式

检查代码是否调用了普通 Android UPI Intent,例如:

intent.setData(Uri.parse("upi://pay?..."));

如果使用的是这种方式,Card Payment 不会出现。需要改用 PhonePe PG SDK 或托管收银台流程。

2. 确认商户是否已开通银行卡通道

即使统一收银台接入无误,如果商户配置没有启用 Card,收银台也可能不显示该选项。需要到 PhonePe 商户后台检查支付方式,或向 PhonePe 商户技术支持确认:

  • Card Payment 是否已开通。
  • 测试环境是否支持 Card。
  • 当前 merchantId 是否具备相应权限。
  • 是否存在行业、账户状态或风控限制。

银行卡通道无法通过客户端代码自行开启。

3. 检查环境是否一致

以下配置必须来自同一个环境:

SDK environment
API base URL
merchantId
saltKey
saltIndex
测试账号或生产账号

使用测试凭证调用生产接口,或使用生产凭证调用测试接口,通常会直接收到后端错误。

4. 记录服务端原始响应

至少记录以下信息,但不要记录完整卡号、CVV、saltKey 或其他敏感数据:

merchantId
merchantTransactionId
API path
HTTP status
PhonePe response code
PhonePe response message
request timestamp
environment

ERROR_B2B_API_RETURNED_ERROR 只是 Android 端显示的通用错误。服务端响应中的 code、message 或 data 更有助于定位具体原因。

5. 主动查询交易状态

使用创建交易时的同一个 merchantTransactionId,从商户服务端调用对应版本的 PhonePe Status API。状态查询请求通常也需要校验签名,不能放在 Android 端完成。

示意接口:

GET /pg/v1/status/{merchantId}/{merchantTransactionId}
X-VERIFY: <server-generated-checksum>
X-MERCHANT-ID: <merchantId>

这只是旧版接口中常见的路径。如果当前文档使用其他路径、OAuth 认证或新版 Standard Checkout API,应严格按照当前版本的定义接入。

6. 将交易标识提交给 PhonePe 支持

如果已经确认请求签名、运行环境和商户权限均无误,但接口仍返回通用错误,可以向 PhonePe 支持提供:

  • merchantId
  • merchantTransactionId
  • 请求时间和时区
  • 测试或生产环境
  • 服务端返回的错误码和消息
  • SDK 版本
  • Android 版本
  • 已脱敏的请求结构

不要提交 saltKey、银行卡号、CVV 或用户的完整敏感信息。

安全与实现注意事项

  • 不要把 saltKey、Client Secret 或其他商户密钥写入 APK。
  • 不要由客户端判断订单支付成功后直接发货。
  • merchantTransactionId 必须保持唯一,并在服务端持久化。
  • 验证回调签名,并防止同一回调被重复处理。
  • 对 PENDING 状态进行延迟查询,不要立即判定为失败。
  • 为同一笔订单设计幂等逻辑,避免重复扣款或重复发货。
  • 不要混用 PhonePe 的 UPI Intent、旧版 PG SDK 和新版 Checkout API 示例。

Card Payment 不能继续使用 UPI App 跳转方案。应先确认商户已经开通银行卡通道,再由服务端创建 PhonePe PG 交易并启动统一收银台。遇到 ERROR_B2B_API_RETURNED_ERROR 时,需要根据服务端原始响应和 Status API 查询结果定位具体原因。

备注:内容仅供参考。