返回

解决PhonePe扣款API "Incorrect Request"报错: 签名与Payload指南

php

搞定 PhonePe Auto-Debit API 的 Incorrect Request 错误

和 PhonePe 的 Auto-Debit API (/v3/recurring/debit/init) 打交道时,你可能也碰到了这个有点让人头疼的错误:

{
  "status": "error",
  "message": "Please check the inputs you have provided. [message = Incorrect Request.]"
}

哪怕你觉得自己已经严格按照官方文档操作了,请求体(payload)看起来也没毛病,比如像下面这样:

{
  "merchantId": "MID12345",
  "merchantUserId": "U123456789",
  "subscriptionId": "OMS2006110139450123456789",
  "transactionId": "TX1234567890",
  "autoDebit": true,
  "amount": 39900
}

还尝试了各种检查:

  • 保证每次请求的 transactionId 都是独一无二的。
  • 确认 amount 用的是 "paisa" 单位(比如 ₹399.00 就是 39900)。
  • 在 Postman 里用写死的值测试过。
  • 核对了 subscriptionId 的格式。
  • 根据 PhonePe 文档生成了 X-VERIFY 签名。
  • 打印日志确认了请求体结构没问题。

结果还是收到 "Incorrect Request"?这到底是 payload 本身的问题,还是 X-VERIFY 签名算错了?咋回事呢?

别急,咱们一步步来分析,找出问题的根源,然后把它解决掉。

原因分析:为啥会报 "Incorrect Request"?

这个错误信息确实有点笼统,它只是告诉你“输入有问题”,但没具体说是哪里出了岔子。根据经验,导致这个错误的原因通常有以下几种:

  1. 请求体 Payload 结构不对劲:
    • 少了必要的字段。
    • 字段的数据类型或格式不符合要求(比如 amount 应该是数字,autoDebit 应该是布尔值)。
    • PhonePe 的 API 要求请求体是一个 JSON 对象,里面包含一个 request 字段,这个字段的值才是你那个业务数据 payload(merchantId, subscriptionId 等)经过 Base64 编码后的字符串。直接发送原始的业务 JSON 是不行的。
  2. X-VERIFY 签名计算错误: 这是最常见的原因之一。
    • 参与计算签名的字符串拼接顺序、内容有误。
    • Base64 编码的对象不对。
    • 使用的 saltKeysaltIndex 不对(比如测试环境用了生产的 Key)。
    • API Endpoint (/v3/recurring/debit/init) 没拼对或者拼错了。
    • 最终的 sha256 哈希计算有误。
  3. HTTP Headers 不全或错误:
    • Content-Type 不是 application/json
    • X-VERIFY Header 格式不对或者丢失。
    • 缺少 X-CALLBACK-URL 这个 Header,对于扣款这种异步操作,回调地址通常是必须的。
  4. 环境配置问题:
    • 使用的 merchantIdsaltKeysaltIndex 和请求的 API Endpoint(比如是测试环境 mercury-t2.phonepe.com 还是生产环境)不匹配。
    • 调用的 subscriptionId 在当前环境无效、未激活,或者不属于该 merchantUserId
  5. subscriptionId 本身有问题:
    • 这个 ID 可能在 PhonePe 系统里不存在、已过期或状态不正确,导致无法发起扣款。

解决方案:逐个击破

既然知道了可能的原因,我们就可以有针对性地排查了。

方案一:仔细核对请求体 Payload 结构和 Base64 编码

原理: API 需要精确的数据结构。不光是业务字段要对,整个请求发送的格式也得符合 PhonePe 的规范,特别是 Base64 封装那层。

操作步骤:

  1. 确认业务字段: 再次打开 PhonePe 最新的官方文档,找到 /v3/recurring/debit/init 这个接口的说明。仔细比对你发送的 JSON(就是包含 merchantId, subscriptionId 那个)里的每一个字段:

    • 必填字段: merchantId, merchantUserId, subscriptionId, transactionId, amount, autoDebit 这些是不是都提供了?有没有漏掉文档里标记为必需的其他字段?(注意:不同接口或版本要求可能微调,务必看准你用的 /v3/recurring/debit/init 的文档)。文档可能会要求 merchantTransactionId 而不是 transactionId,请核对清楚。这里以你的示例为准,但强烈建议核对官方文档。
    • 数据类型: amount 是数字 (integer),autoDebit 是布尔值 (true/false),其他是字符串 (string)。确保类型无误。
    • 值格式: amount 单位是 paisa,确认没问题。transactionId 必须是每次唯一的。
  2. 确认最终发送结构: 这是关键!你不能直接把上面的业务 JSON 作为 HTTP 请求体发送。你需要:

    • 先把业务 JSON(包含 merchantId 等)转换成字符串。
    • 对这个字符串进行 Base64 编码。
    • 构造一个新的 JSON 对象,像这样: {"request": "这里放Base64编码后的字符串"}
    • 把这个新的 JSON 对象作为最终的 HTTP POST 请求体发送出去。

代码示例(PHP - 对应你提供的示例):

<?php
// 业务 Payload
$authPayload_1 = [
  'merchantId' => 'your_merchant_id',          // 你的商户ID
  'merchantUserId' => 'your_merchant_user_id', // 你的用户ID
  'subscriptionId' => 'SUB123456',             // 订阅ID
  'transactionId' => 'TX' . strtoupper(bin2hex(random_bytes(8))), // 唯一交易ID
  'autoDebit' => true,                          // 自动扣款标识
  'amount' => 10000,                           // 金额 (paisa), 这里是 ₹100
];

// 1. 将业务 Payload 转成 JSON 字符串
$jsonPayload = json_encode($authPayload_1);

// 2. 对 JSON 字符串进行 Base64 编码
$base64Payload = base64_encode($jsonPayload);

// 3. 构建最终要发送的请求体结构
$finalRequestBody = json_encode(['request' => $base64Payload]); // 注意这里!

// 在 cURL 中设置这个最终的请求体
// curl_setopt($curl, CURLOPT_POSTFIELDS, $finalRequestBody); // 这才是要POST出去的数据
?>

检查点:

  • 确保你的代码确实执行了 Base64 编码。
  • 确认编码的对象是 业务 JSON 字符串 ,而不是 PHP 数组或其他东西。
  • 确认最终发送的是包含 request 键的 JSON,其值是 Base64 串。可以打印日志 $finalRequestBody 来验证。

方案二:精确校验 X-VERIFY 签名生成

原理: X-VERIFY 签名是 PhonePe 用来验证请求来源可靠性和数据完整性的机制。一点小差错就会导致验证失败,从而报 "Incorrect Request"。

操作步骤:

  1. 理解签名公式: PhonePe 的签名公式通常是:
    SHA256(Base64(业务 Payload JSON) + apiEndpoint + saltKey) + "###" + saltIndex

  2. 分解校验每个部分:

    • Base64(业务 Payload JSON): 这部分必须是你 步骤一 中生成的那个 base64Payload 字符串。务必保证和实际放入 request 字段的值完全一致。
    • apiEndpoint: 对于这个接口,它固定是字符串 /v3/recurring/debit/init。检查有没有多余的斜杠、空格,或者写成了完整的 URL。
    • saltKey: 这是 PhonePe 提供给你的密钥。确认你用的是 对应环境 (测试/生产)的 正确 Salt Key。通常是一长串随机字符。
    • saltIndex: 这是与 saltKey 配对的索引号,通常是个数字,也是 PhonePe 提供的。确保用了正确的 Index。
  3. 拼接: 将上述三个部分 严格按照顺序 拼接成一个字符串。中间没有任何分隔符。

  4. 计算 SHA256 哈希: 对拼接好的字符串计算 sha256 哈希值。确保输出的是小写字母的哈希字符串。

  5. 最终拼接: 将计算出的 sha256 哈希字符串,加上 ###,再加上你的 saltIndex,组成最终的 X-VERIFY Header 值。

代码示例(PHP - 对应你提供的示例):

<?php
// ... (接上面的 $authPayload_1, $base64Payload) ...

$apiKey = 'your_salt_key'; // 你的 Salt Key
$saltIndex = 'your_salt_index'; // 你的 Salt Index (通常是 1 或其他数字)
$apiEndpoint = '/v3/recurring/debit/init'; // 接口路径

// 1. 准备待签名字符串
$stringToHash = $base64Payload . $apiEndpoint . $apiKey;

// 2. 计算 SHA256 哈希
$sha256Hash = hash('sha256', $stringToHash);

// 3. 拼接成最终的 X-VERIFY Header 值
$xVerifyValue = $sha256Hash . '###' . $saltIndex;

// 在 cURL 中设置 Header
// 'X-VERIFY: ' . $xVerifyValue
?>

调试技巧:

  • 打印中间值: 在代码里把 base64Payload, apiEndpoint, apiKey, stringToHash, sha256Hash, xVerifyValue 全都打印出来。
  • 手动验证: 找一个在线的 SHA256 计算工具,把你打印出来的 stringToHash 复制进去计算,看结果和你代码生成的 sha256Hash 是否一致。注意编码问题,确保在线工具处理的是原始字节或与 PHP hash() 函数行为一致。
  • 核对 Key/Index: 反复确认 apiKeysaltIndex 真的没填错,尤其是从配置或环境变量读取时。

安全建议:

  • 绝不硬编码: 不要把 saltKeysaltIndex 直接写在代码里。用环境变量或者安全的配置文件管理。
  • 最小权限: 确保只有必要的服务能访问到这些敏感信息。

方案三:检查必要的 HTTP Headers

原理: API 网关或服务器依赖 Headers 来理解请求内容、验证身份和知道如何响应(比如回调)。

操作步骤:

  1. 检查必备 Headers: 确保你的 HTTP 请求里包含了以下 Headers:
    • Content-Type: application/json:告诉服务器你发送的是 JSON 数据(即使主体是 {"request": "..."},外面这层也是 JSON)。
    • X-VERIFY: YOUR_GENERATED_SIGNATURE:值为你 方案二 中生成的那个签名字符串。
    • X-CALLBACK-URL: https://yourdomain.com/api/phonepe/callback:提供一个 公网可以访问的 HTTPS 地址 ,用于接收 PhonePe 的异步扣款结果通知。这个 URL 很重要,如果漏了或者格式不对(比如用了 HTTP),都可能导致请求失败或后续流程出问题。

代码示例(PHP - 对应你提供的示例中的 cURL 设置):

<?php
// ... (前面的代码) ...
$callurl = "https://yourdomain.com/api/phonepe/callback"; // 你的回调URL,必须是HTTPS

$curl = curl_init();
curl_setopt_array($curl, [
  CURLOPT_URL => 'https://mercury-t2.phonepe.com/v3/recurring/debit/init', // 确认环境URL
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => $finalRequestBody, // 使用包含 'request' 键的JSON
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',      // 必须
    'X-VERIFY: ' . $xVerifyValue,           // 必须,值来自方案二
    'X-CALLBACK-URL: '. $callurl           // 对于扣款通常必须
  ],
]);

// ... (执行 cURL, 处理响应) ...
?>

检查点:

  • 回调 URL 是否真的是 HTTPS?浏览器能正常访问吗?
  • Headers 的名字(Content-Type, X-VERIFY, X-CALLBACK-URL)有没有拼写错误?
  • Header 的值格式是否正确?(比如 X-VERIFYhash###index

方案四:核对环境和关联 ID

原理: 测试环境(Sandbox/UAT)和生产环境是隔离的,它们有不同的 API Endpoint、不同的 merchantId、不同的 Key 和 Index。混用会导致验证失败。同时,订阅 ID 也需要在对应环境真实有效。

操作步骤:

  1. 环境一致性: 检查你使用的:

    • API Endpoint URL (https://mercury-t2.phonepe.com 是测试环境,生产环境 URL 不同)。
    • merchantId
    • saltKey (apiKey 变量的值)。
    • saltIndex ($saltIndex 变量的值)。
      确保这四项 完全匹配 你当前想要调用的环境(测试或生产)。
  2. subscriptionId 有效性:

    • 确认这个 subscriptionId 是在 当前环境 下成功创建并且状态是 ACTIVE (或允许扣款的状态) 的。
    • 确认这个 subscriptionId 确实是属于请求中提供的 merchantUserId 的。

检查点:

  • 是不是错把生产的 Key 用到了测试环境,或者反过来?
  • 这个 subscriptionId 是不是刚刚创建还未激活,或者已经被取消了?
  • 如果你在测试一个特定的 merchantUserId,确保这个 subscriptionId 确实关联到了这个用户。

通过上面这几个方案逐一排查,基本就能覆盖掉导致 /v3/recurring/debit/init 接口报 "Incorrect Request" 的常见原因了。耐心点,一步步来,特别是签名那块,仔细核对每一个细节。祝你顺利搞定!