w7corp/easywechat · error · InvalidConfigException

No platform certs found for serial: {$serial},

Error message

No platform certs found for serial: {$serial}, 
                please download from wechat pay and set it in merchant config with key `certs`.

What it means

After the header and timestamp checks, Validator::validate() must verify the signature with the WeChat Pay platform public key whose certificate serial matches the Wechatpay-Serial header (src/Pay/Validator.php:53-61). Merchant::getPlatformCert() looks the cert up in the platformCerts map (src/Pay/Merchant.php:62-65); when nothing is registered for that serial it throws InvalidConfigException. The map comes from the `platform_certs` config (src/Pay/Application.php:47), and list entries are keyed by their real certificate serial automatically (src/Pay/Merchant.php:91). Note: the message's hint `certs` is stale wording — the config key in this codebase is `platform_certs`.

Source

Thrown at src/Pay/Validator.php:56

        }

        [$timestamp] = $message->getHeader(self::HEADER_TIMESTAMP);
        [$nonce] = $message->getHeader(self::HEADER_NONCE);
        [$serial] = $message->getHeader(self::HEADER_SERIAL);
        [$signature] = $message->getHeader(self::HEADER_SIGNATURE);

        $body = (string) $message->getBody();

        $message = "{$timestamp}\n{$nonce}\n{$body}\n";

        if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
            throw new InvalidSignatureException('Clock Offset Exceeded');
        }

        $publicKey = $this->merchant->getPlatformCert($serial);

        if (! $publicKey) {
            throw new InvalidConfigException(
                "No platform certs found for serial: {$serial}, 
                please download from wechat pay and set it in merchant config with key `certs`."
            );
        }

        if (\openssl_verify(
            $message,
            base64_decode($signature),
            strval($publicKey),
            OPENSSL_ALGO_SHA256
        ) !== 1) {
            throw new InvalidSignatureException('Invalid Signature');
        }
    }
}

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Fetch the current platform certs via the v3 certificates API (GET /v3/certificates using the SDK client, decrypt with secret_key via AES-256-GCM) and put the PEM public keys into `platform_certs`.
  2. Or download the platform certificate from the WeChat Pay merchant console and add it: 'platform_certs' => ['-----BEGIN CERTIFICATE-----...'] — serials are derived from the certs themselves for list entries.
  3. Keep superseded certs configured alongside new ones so a rotation never leaves a serial unmapped.
  4. Add a runtime fallback: on this exception, re-download certs, persist them (config/cache), and retry validation exactly once.

Example fix

// before: v3 credentials only
$config = [
    'mch_id' => '1900000000',
    'secret_key' => '<api-v3-key>',
    'private_key' => '...',
    'certificate' => '...',
];
// first webhook -> "No platform certs found for serial: 5157F09..."

// after: register platform certs (serial keys derived automatically)
$config = [
    'mch_id' => '1900000000',
    'secret_key' => '<api-v3-key>',
    'private_key' => '...',
    'certificate' => '...',
    'platform_certs' => [
        '-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n',
        '-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n', // keep old + new across rotations
    ],
];
Defensive patterns

Strategy: fallback

Validate before calling

$serial = $request->getHeaderLine('Wechatpay-Serial');

if ($serial !== '' && $app->getMerchant()->getPlatformCert($serial) === null) {
    // Serial not mapped: platform certs missing or WeChat rotated them.
    // GET /v3/certificates, decrypt with secret_key (AES-256-GCM),
    // persist the public keys into `platform_certs`, rebuild the merchant,
    // then continue.
    refreshPlatformCertsFromApi($app);
}

$app->getValidator()->validate($request);

Type guard

use EasyWeChat\Kernel\Support\PublicKey;
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;

function hasPlatformCertForSerial(MerchantInterface $merchant, string $serial): bool
{
    return $merchant->getPlatformCert($serial) instanceof PublicKey;
}

Try / catch

use EasyWeChat\Kernel\Exceptions\InvalidConfigException;

try {
    $app->getValidator()->validate($request);
} catch (InvalidConfigException $e) {
    if (str_contains($e->getMessage(), 'platform certs')) {
        refreshPlatformCertsFromApi($app); // download + persist, keeping old certs mapped

        return $app->getValidator()->validate($request); // retry exactly once
    }

    throw $e;
}

Prevention

When it happens

Trigger: First webhook or first response validation before any platform cert was configured; WeChat Pay rotated its platform certificates so the new serial in Wechatpay-Serial has no matching entry; platform_certs passed as a map with hand-written wrong serial keys; multi-merchant apps sharing one cert set across different merchant accounts.

Common situations: New integrations that set only private_key, certificate and secret_key; production working for months then breaking after WeChat's periodic platform-cert rotation; environment-specific config files missing platform_certs; confusing the merchant certificate with the platform certificate.

Related errors


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