w7corp/easywechat · error · InvalidArgumentException
V2 secret key is required.
Error message
V2 secret key is required.
What it means
Thrown by Pay/Server::decodeXmlMessage() when a V2 XML callback contains a req_info element (encrypted refund/negotiate data) but the merchant has no V2 API key configured. req_info is AES-256-ECB encrypted with md5(v2Key); decrypting is impossible without the key.
Source
Thrown at src/Pay/Server.php:226
}
/**
* @throws InvalidArgumentException
* @throws RuntimeException
*/
protected function decodeXmlMessage(string $contents): array
{
$attributes = Xml::parse($contents);
if (! is_array($attributes)) {
throw new RuntimeException('Invalid request body.');
}
if (! empty($attributes['req_info'])) {
$key = $this->merchant->getV2SecretKey();
if (empty($key)) {
throw new InvalidArgumentException('V2 secret key is required.');
}
$attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));
}
if (
is_array($attributes)
&& array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])
&& array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])
&& array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])
) {
$attributes += Xml::parse(AesGcm::decrypt(
$attributes['event_ciphertext'],
$this->merchant->getSecretKey(),
$attributes['event_nonce'],
$attributes['event_associated_data'] // maybe empty string
));
}View on GitHub (pinned to f0cf0a8b83)
Solutions
- Confirm the 32-char APIv2 key is set in the merchant console (API security -> APIv2 key)
- Pass it as v2SecretKey when constructing Merchant (5th parameter) or via your config binding
- Migrate the callback to the V3 notification URL if you want to drop V2 keys entirely
Example fix
// before $merchant = new Merchant($mchId, $privateKey, $certificate, $secretKey); $app->server->handlePaidCallback(...); // V2 refund push -> throws // after $merchant = new Merchant($mchId, $privateKey, $certificate, $secretKey, $v2SecretKey);
Defensive patterns
Strategy: validation
Validate before calling
if (str_contains($request->getHeaderLine('content-type'), 'xml')
&& $merchant->getV2SecretKey() === null) {
throw new \RuntimeException('V2 refund callbacks require the APIv2 key');
} Type guard
function hasV2Key(\EasyWeChat\Pay\Merchant $m): bool
{
return is_string($m->getV2SecretKey()) && $m->getV2SecretKey() !== '';
} Try / catch
try {
$app->server->handlePaidCallback($fn);
} catch (\EasyWeChat\Kernel\Exceptions\InvalidArgumentException $e) {
if ($e->getMessage() === 'V2 secret key is required.') {
// alert: legacy notify URL still active without V2 key; migrate to V3 or set key
}
} Prevention
- Construct Merchant with v2SecretKey whenever legacy notify URLs exist
- Audit old V2 notify URLs after migrating to V3 and switch them off
- Config smoke-test both keys at boot
When it happens
Trigger: Receiving a V2 refund notification (pay/refund push with <req_info>) or account-change callback while Merchant::getV2SecretKey() returns null/'' — e.g. constructed without the fifth v2SecretKey argument.
Common situations: V3-first integrations that still receive legacy refund callbacks on the old notify URL; the V2 key set in the console but never wired into the Merchant config; config key named differently (v2_secret vs secret_v2) so it reads as null.
Related errors
- Missing V2 API key.
- Missing platform certificate.
- Invalid platform certficate.
- Invalid event type.
- Invalid request body.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/5347e9785e5f61c2.
Report an issue: GitHub.