Guide to WeChat Pay Webhooks: Features and Best Practices
WeChat Pay notifications tell your systems about payment activity: a transaction succeeds, a refund completes. If you're integrating WeChat Pay (APIv3), these notifications are how you react to money movement without polling, but each one requires two cryptographic steps, not just a signature check.
This guide covers how WeChat Pay APIv3 notifications work, how to verify the RSA signature, how to decrypt the encrypted payload, certificate rotation, and the best practices for production.
What are WeChat Pay webhooks?
WeChat Pay APIv3 notifications are HTTP POSTs delivered to a notify_url you set per API request. Two things make them demanding:
- Asymmetric signature: each notification is signed with SHA256withRSA, verified with WeChat Pay's platform public certificate (not your merchant cert), matched by the
Wechatpay-Serialheader. - Encrypted payload: the notification's
resourceis AES-256-GCM ciphertext. The signed body is the ciphertext, not the transaction JSON; you decrypt it with your APIv3 key to recover the data.
Both steps are load-bearing.
WeChat Pay webhook features
| Feature | Details |
|---|---|
| Configuration | notify_url set per API request |
| Signature headers | Wechatpay-Signature (base64), Wechatpay-Timestamp, Wechatpay-Nonce, Wechatpay-Serial |
| Signature scheme | SHA256withRSA over {timestamp}\n{nonce}\n{body}\n, platform public cert by serial |
| Payload encryption | resource.ciphertext is AEAD_AES_256_GCM; decrypt with the 32-byte APIv3 key |
| Certificate rotation | Poll GET /v3/certificates (new certs added ~24h ahead) |
| Replay check | Reject timestamps more than 5 minutes off |
| Acknowledgement | HTTP 200/204 with {"code":"SUCCESS","message":"OK"} |
| Retries | ~15s to 6h over ~24h |
| SDK | Community only (wechatpay-node-v3, wechatpayv3) |
Common events
The notification's outer event_type names what happened; the decrypted resource carries the detail:
| Event | Fires when |
|---|---|
TRANSACTION.SUCCESS | A transaction succeeds |
REFUND.SUCCESS | A refund completes |
REFUND.CLOSED | A refund is closed |
Note: REFUND.ABNORMAL appears only in the mainland-China docs, not the global English APIv3 set (which uses REFUND.SUCCESS / REFUND.CLOSED). Confirm which endpoint your integration targets.
Setting up WeChat Pay webhooks
Set the notify_url per API request. Maintain WeChat Pay's platform public certificates by polling GET /v3/certificates (itself AES-GCM encrypted), matching the Wechatpay-Serial header to the right cert; WeChat adds new certs about 24 hours ahead of rotation. Acknowledge each notification with HTTP 200/204 and {"code":"SUCCESS","message":"OK"}; otherwise WeChat retries from ~15 seconds up to ~6 hours over about 24 hours.
Securing WeChat Pay webhooks
Step 1: verify the signature
Reconstruct the signed message as {Wechatpay-Timestamp}\n{Wechatpay-Nonce}\n{body}\n (each line ending in \n, body = raw bytes), base64-decode Wechatpay-Signature, and verify with SHA256withRSA using the platform public key matched by Wechatpay-Serial. Reject timestamps more than 5 minutes off.
Step 2: decrypt the resource
The resource object is {ciphertext, nonce, associated_data, algorithm: "AEAD_AES_256_GCM"}. Decrypt ciphertext (base64) with your 32-byte APIv3 key, using nonce as the IV and associated_data as the AAD.
const crypto = require("crypto");
const API_V3_KEY = process.env.WECHAT_APIV3_KEY; // 32 bytes
// Step 1: verify SHA256withRSA over timestamp\nnonce\nbody\n
function verify(headers, rawBody, platformPublicKey) {
const timestamp = headers["wechatpay-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5 min
const message = `${timestamp}\n${headers["wechatpay-nonce"]}\n${rawBody}\n`;
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(message);
return verifier.verify(
platformPublicKey, // matched by headers["wechatpay-serial"]
Buffer.from(headers["wechatpay-signature"], "base64")
);
}
// Step 2: AES-256-GCM decrypt the resource
function decryptResource(resource) {
const data = Buffer.from(resource.ciphertext, "base64");
const authTag = data.subarray(data.length - 16); // last 16 bytes
const ciphertext = data.subarray(0, data.length - 16);
const decipher = crypto.createDecipheriv("aes-256-gcm", API_V3_KEY, resource.nonce);
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(resource.associated_data));
return JSON.parse(
Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8")
);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const platformKey = getPlatformKey(req.headers["wechatpay-serial"]); // cached, rotated
if (!verify(req.headers, req.body.toString(), platformKey)) {
return res.status(401).json({ code: "FAIL", message: "invalid signature" });
}
const notification = JSON.parse(req.body);
const transaction = decryptResource(notification.resource);
res.status(200).json({ code: "SUCCESS", message: "OK" });
processQueue.add({ event: notification.event_type, transaction }); // async
});
WeChat Pay webhook limitations and pain points
Two cryptographic steps per notification
The Problem: Verifying the RSA signature isn't enough; the useful data is inside an AES-256-GCM ciphertext you must decrypt with a separate key. Handlers that stop at signature verification never see the transaction.
Why It Happens: WeChat Pay signs the notification envelope and encrypts the resource separately.
Workarounds:
- Verify the signature, then decrypt
resource.ciphertextwith the APIv3 key (nonce + associated_data).
How Hookdeck Can Help: Hookdeck verifies the signature at the edge, so your app receives pre-verified notifications; you still hold the APIv3 key to decrypt, but the verification burden is offloaded.
Certificate rotation
The Problem: The signing cert rotates. Matching Wechatpay-Serial to the wrong (or stale) platform cert fails verification, and WeChat adds new certs ~24h ahead.
Why It Happens: WeChat Pay rotates platform certificates and identifies them by serial.
Workarounds:
- Poll
GET /v3/certificates, cache platform certs by serial, and select byWechatpay-Serial.
How Hookdeck Can Help: Hookdeck tracks the rotating certificates and verifies each notification, so rotation doesn't surface as a verification outage in your app.
The signed body is the raw bytes
The Problem: The signature is over {timestamp}\n{nonce}\n{body}\n where body is the raw bytes. Re-serializing the JSON breaks verification.
Why It Happens: WeChat signs the exact received body.
Workarounds:
- Capture the raw body and reconstruct the message exactly, with trailing
\n.
How Hookdeck Can Help: Hookdeck verifies against the bytes as received.
Ack format and retries
The Problem: WeChat expects {"code":"SUCCESS","message":"OK"}; an incorrect ack triggers retries over ~24 hours, so notifications repeat.
Why It Happens: WeChat keys retries on the structured acknowledgement.
Workarounds:
- Return the exact success body, and dedupe/idempotently process retries; re-verify the amount before fulfilling.
How Hookdeck Can Help: Hookdeck acknowledges correctly and deduplicates, so retries don't double-process. See our guide to webhook idempotency.
Best practices
Verify the signature, then decrypt
Verify SHA256withRSA over {timestamp}\n{nonce}\n{body}\n with the serial-matched platform cert, then AES-256-GCM-decrypt resource.ciphertext with the APIv3 key.
Rotate certificates
Poll GET /v3/certificates, cache by serial, and select by Wechatpay-Serial.
Return the exact success ack
Reply {"code":"SUCCESS","message":"OK"} to stop retries.
Re-verify the amount and dedupe
Confirm the order amount before fulfilling, and dedupe across the ~24-hour retry window.
Make WeChat Pay webhooks production-ready
Hookdeck verifies the RSA signature, deduplicates, and durably queues every payment notification
Conclusion
WeChat Pay APIv3 notifications demand two steps: verify the SHA256withRSA signature over {timestamp}\n{nonce}\n{body}\n with the serial-matched platform certificate, then AES-256-GCM-decrypt resource.ciphertext with your APIv3 key. Add certificate rotation, a 5-minute replay window, a structured ack, and amount re-verification, and there's real cryptographic work to get right.
Hookdeck verifies the signature and tracks certificate rotation at the edge, and deduplicates and durably queues every notification, so your app processes verified, unique payment events with far less crypto plumbing.
Get started with Hookdeck for free and handle WeChat Pay webhooks reliably in minutes.