解决PhonePe扣款API "Incorrect Request"报错: 签名与Payload指南
2025-04-21 08:08:25
搞定 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"?
这个错误信息确实有点笼统,它只是告诉你“输入有问题”,但没具体说是哪里出了岔子。根据经验,导致这个错误的原因通常有以下几种:
- 请求体 Payload 结构不对劲:
- 少了必要的字段。
- 字段的数据类型或格式不符合要求(比如
amount
应该是数字,autoDebit
应该是布尔值)。 - PhonePe 的 API 要求请求体是一个 JSON 对象,里面包含一个
request
字段,这个字段的值才是你那个业务数据 payload(merchantId
,subscriptionId
等)经过 Base64 编码后的字符串。直接发送原始的业务 JSON 是不行的。
- X-VERIFY 签名计算错误: 这是最常见的原因之一。
- 参与计算签名的字符串拼接顺序、内容有误。
- Base64 编码的对象不对。
- 使用的
saltKey
或saltIndex
不对(比如测试环境用了生产的 Key)。 - API Endpoint (
/v3/recurring/debit/init
) 没拼对或者拼错了。 - 最终的
sha256
哈希计算有误。
- HTTP Headers 不全或错误:
Content-Type
不是application/json
。X-VERIFY
Header 格式不对或者丢失。- 缺少
X-CALLBACK-URL
这个 Header,对于扣款这种异步操作,回调地址通常是必须的。
- 环境配置问题:
- 使用的
merchantId
、saltKey
、saltIndex
和请求的 API Endpoint(比如是测试环境mercury-t2.phonepe.com
还是生产环境)不匹配。 - 调用的
subscriptionId
在当前环境无效、未激活,或者不属于该merchantUserId
。
- 使用的
subscriptionId
本身有问题:- 这个 ID 可能在 PhonePe 系统里不存在、已过期或状态不正确,导致无法发起扣款。
解决方案:逐个击破
既然知道了可能的原因,我们就可以有针对性地排查了。
方案一:仔细核对请求体 Payload 结构和 Base64 编码
原理: API 需要精确的数据结构。不光是业务字段要对,整个请求发送的格式也得符合 PhonePe 的规范,特别是 Base64 封装那层。
操作步骤:
-
确认业务字段: 再次打开 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
必须是每次唯一的。
- 必填字段:
-
确认最终发送结构: 这是关键!你不能直接把上面的业务 JSON 作为 HTTP 请求体发送。你需要:
- 先把业务 JSON(包含
merchantId
等)转换成字符串。 - 对这个字符串进行 Base64 编码。
- 构造一个新的 JSON 对象,像这样:
{"request": "这里放Base64编码后的字符串"}
。 - 把这个新的 JSON 对象作为最终的 HTTP POST 请求体发送出去。
- 先把业务 JSON(包含
代码示例(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"。
操作步骤:
-
理解签名公式: PhonePe 的签名公式通常是:
SHA256(Base64(业务 Payload JSON) + apiEndpoint + saltKey) + "###" + saltIndex
-
分解校验每个部分:
Base64(业务 Payload JSON)
: 这部分必须是你 步骤一 中生成的那个base64Payload
字符串。务必保证和实际放入request
字段的值完全一致。apiEndpoint
: 对于这个接口,它固定是字符串/v3/recurring/debit/init
。检查有没有多余的斜杠、空格,或者写成了完整的 URL。saltKey
: 这是 PhonePe 提供给你的密钥。确认你用的是 对应环境 (测试/生产)的 正确 Salt Key。通常是一长串随机字符。saltIndex
: 这是与saltKey
配对的索引号,通常是个数字,也是 PhonePe 提供的。确保用了正确的 Index。
-
拼接: 将上述三个部分 严格按照顺序 拼接成一个字符串。中间没有任何分隔符。
-
计算 SHA256 哈希: 对拼接好的字符串计算
sha256
哈希值。确保输出的是小写字母的哈希字符串。 -
最终拼接: 将计算出的
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
是否一致。注意编码问题,确保在线工具处理的是原始字节或与 PHPhash()
函数行为一致。 - 核对 Key/Index: 反复确认
apiKey
和saltIndex
真的没填错,尤其是从配置或环境变量读取时。
安全建议:
- 绝不硬编码: 不要把
saltKey
和saltIndex
直接写在代码里。用环境变量或者安全的配置文件管理。 - 最小权限: 确保只有必要的服务能访问到这些敏感信息。
方案三:检查必要的 HTTP Headers
原理: API 网关或服务器依赖 Headers 来理解请求内容、验证身份和知道如何响应(比如回调)。
操作步骤:
- 检查必备 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-VERIFY
是hash###index
)
方案四:核对环境和关联 ID
原理: 测试环境(Sandbox/UAT)和生产环境是隔离的,它们有不同的 API Endpoint、不同的 merchantId
、不同的 Key 和 Index。混用会导致验证失败。同时,订阅 ID 也需要在对应环境真实有效。
操作步骤:
-
环境一致性: 检查你使用的:
- API Endpoint URL (
https://mercury-t2.phonepe.com
是测试环境,生产环境 URL 不同)。 merchantId
。saltKey
(apiKey
变量的值)。saltIndex
($saltIndex
变量的值)。
确保这四项 完全匹配 你当前想要调用的环境(测试或生产)。
- API Endpoint URL (
-
subscriptionId
有效性:- 确认这个
subscriptionId
是在 当前环境 下成功创建并且状态是 ACTIVE (或允许扣款的状态) 的。 - 确认这个
subscriptionId
确实是属于请求中提供的merchantUserId
的。
- 确认这个
检查点:
- 是不是错把生产的 Key 用到了测试环境,或者反过来?
- 这个
subscriptionId
是不是刚刚创建还未激活,或者已经被取消了? - 如果你在测试一个特定的
merchantUserId
,确保这个subscriptionId
确实关联到了这个用户。
通过上面这几个方案逐一排查,基本就能覆盖掉导致 /v3/recurring/debit/init
接口报 "Incorrect Request" 的常见原因了。耐心点,一步步来,特别是签名那块,仔细核对每一个细节。祝你顺利搞定!