Payment gateway webhooks notify your application of payment events (successful charges, refunds, disputes) — correctly and securely handling these is essential for accurate order processing. This guide covers proper implementation.
Why Payment Webhooks Need Special Security Attention
See How to Design and Secure Webhook Endpoints for general webhook security — payment webhooks carry particular stakes: an improperly verified webhook could let an attacker fake a "payment successful" notification, potentially resulting in orders being fulfilled without genuine payment.
Always Verify Webhook Signatures
const signature = req.headers['x-payment-gateway-signature'];
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature');
}
Every major payment gateway provides a signature verification mechanism — never process a webhook without verifying its authenticity; this is not optional for payment-related webhooks given the genuine financial stakes.
Using the Raw Request Body for Signature Verification
Signature verification typically requires the exact raw request body (not a re-serialized/parsed version) — ensure your framework preserves and provides access to the genuine raw body for this verification step, since re-serialization can subtly alter the bytes and break signature matching.
Never Trust Client-Side Payment Confirmation Alone
A frontend "payment successful" callback/redirect should never be your sole source of truth for order fulfillment — always confirm via the server-to-server webhook (or an API status check) before actually fulfilling an order; client-side confirmations can be manipulated or fail to fire reliably.
Handling Idempotency for Webhook Delivery
See How to Implement Idempotent API Endpoints — payment gateways may deliver the same webhook event multiple times (retries, at-least-once delivery guarantees); ensure processing a duplicate webhook doesn't result in double-fulfillment or duplicate charges/effects.
Processing Different Event Types Appropriately
switch (event.type) {
case 'payment.succeeded':
await fulfillOrder(event.data.orderId);
break;
case 'payment.failed':
await markOrderFailed(event.data.orderId);
break;
case 'charge.refunded':
await processRefund(event.data.orderId);
break;
}
Handle each event type your business logic actually needs — don't just process success events; failure and refund events are equally important for accurate order/customer state.
Responding Quickly to Avoid Gateway Timeout/Retry
app.post('/webhook/payment', async (req, res) => {
res.status(200).send();
await processWebhookAsync(req.body);
});
See How to Design a Webhook Retry Strategy for the sender-side perspective — respond quickly to acknowledge receipt, processing the actual business logic asynchronously if it might take longer than the gateway's expected response timeout.
Logging Webhook Events for Audit and Debugging
Maintain a record of received webhook events (particularly for payment-related ones) — valuable for debugging discrepancies, handling disputes, and maintaining a genuine audit trail of payment-related activity.
Testing Webhooks in a Staging Environment
Most payment gateways provide test/sandbox webhook capability — thoroughly test your webhook handling logic (including signature verification, idempotency, and various event types) in a staging environment before relying on it in production.
Common Errors
Signature verification consistently fails despite correct secret — almost always a raw body access issue; verify your web framework isn't parsing/re-serializing the request body before your signature verification code runs, which would break the byte-exact comparison signature verification requires.
Continue Reading
- How to Design and Secure Webhook Endpoints
- How to Implement Idempotent API Endpoints
- E-commerce Security Checklist: Protecting Customer Data on a VPS
Browse more articles in E-commerce Platform Deployment.