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

  1. Confirm the 32-char APIv2 key is set in the merchant console (API security -> APIv2 key)
  2. Pass it as v2SecretKey when constructing Merchant (5th parameter) or via your config binding
  3. 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

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


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/5347e9785e5f61c2. Report an issue: GitHub.