{"record":{"id":"5347e9785e5f61c2","repo":"w7corp/easywechat","slug":"v2-secret-key-is-required","errorCode":null,"errorMessage":"V2 secret key is required.","messagePattern":"V2 secret key is required\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"src/Pay/Server.php","lineNumber":226,"sourceCode":"    }\n\n    /**\n     * @throws InvalidArgumentException\n     * @throws RuntimeException\n     */\n    protected function decodeXmlMessage(string $contents): array\n    {\n        $attributes = Xml::parse($contents);\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Invalid request body.');\n        }\n\n        if (! empty($attributes['req_info'])) {\n            $key = $this->merchant->getV2SecretKey();\n\n            if (empty($key)) {\n                throw new InvalidArgumentException('V2 secret key is required.');\n            }\n\n            $attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));\n        }\n\n        if (\n            is_array($attributes)\n            && array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])\n            && array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])\n            && array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])\n        ) {\n            $attributes += Xml::parse(AesGcm::decrypt(\n                $attributes['event_ciphertext'],\n                $this->merchant->getSecretKey(),\n                $attributes['event_nonce'],\n                $attributes['event_associated_data'] // maybe empty string\n            ));\n        }","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Pay/Server.php#L208-L244","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n$merchant = new Merchant($mchId, $privateKey, $certificate, $secretKey);\n$app->server->handlePaidCallback(...); // V2 refund push -> throws\n// after\n$merchant = new Merchant($mchId, $privateKey, $certificate, $secretKey, $v2SecretKey);","handlingStrategy":"validation","validationCode":"if (str_contains($request->getHeaderLine('content-type'), 'xml')\n    && $merchant->getV2SecretKey() === null) {\n    throw new \\RuntimeException('V2 refund callbacks require the APIv2 key');\n}","typeGuard":"function hasV2Key(\\EasyWeChat\\Pay\\Merchant $m): bool\n{\n    return is_string($m->getV2SecretKey()) && $m->getV2SecretKey() !== '';\n}","tryCatchPattern":"try {\n    $app->server->handlePaidCallback($fn);\n} catch (\\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException $e) {\n    if ($e->getMessage() === 'V2 secret key is required.') {\n        // alert: legacy notify URL still active without V2 key; migrate to V3 or set key\n    }\n}","preventionTips":["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"],"tags":["wechat-pay","v2-api-key","webhook","merchant-config"],"backgroundTag":"missing-api-key","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}