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
- 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`.
- 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.
- Keep superseded certs configured alongside new ones so a rotation never leaves a serial unmapped.
- 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
- Provision platform_certs during deployment, not lazily on the first webhook
- After fetching /v3/certificates, keep superseded certs mapped until rotation fully completes
- Automate cert refresh on a schedule and on unknown-serial errors
- Give each merchant account its own platform_certs set in multi-merchant apps
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
- Missing v2 secret key.
- Missing Header: {$header}
- Clock Offset Exceeded
- Invalid Signature
- Encrypt AES ECB failed.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/8590fadca35442e5.
Report an issue: GitHub.