Error Handling
Always implement comprehensive error handling when processing payments:
const response = await stripe.processPayment(100, 'usd');
if (!response.success) {
throw new Error(response.error?.message);
}
// Handle specific error codes for user-friendly behavior
switch (response.error?.code) {
case 'card_declined':
console.warn('Card was declined. Prompt user to try another card.');
break;
case 'insufficient_funds':
console.warn('Insufficient funds. Suggest alternative payment methods.');
break;
case 'authentication_required':
console.warn('3D Secure authentication required. Guide user through the flow.');
break;
default:
console.warn('Unexpected payment error:', response.error?.message);
break;
}
Retry Logic
Use exponential backoff for transient failures like network errors or gateway timeouts:
async function processPaymentWithRetry(
gateway: any,
amount: number,
currency: string,
maxRetries = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await gateway.processPayment(amount, currency);
if (result.success) return result;
// Do not retry permanent failures
if (['card_declined', 'authentication_required'].includes(result.error?.code || '')) {
throw new Error(result.error?.message);
}
} catch (error) {
if (attempt === maxRetries) throw error;
// Exponential backoff delay
const delay = Math.pow(2, attempt) * 1000;
console.info(`Retrying payment in ${delay / 1000}s...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Webhook Security
Stripe - Simplified Webhook Handling
For Stripe, use the built-in handleWebhookEvent
method that automatically handles signature verification:
import express from 'express';
const app = express();
// IMPORTANT: Use raw body parsing for webhook signature verification
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook/stripe', async (req, res) => {
try {
const result = await stripe.handleWebhookEvent(req.body, req.headers);
res.status(result === 'INVALID' ? 400 : 200).send(result);
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Webhook processing failed');
}
});
The handleWebhookEvent
method returns:
"VERIFIED"
- Event verified and processed successfully"UNHANDLED"
- Event verified but no handler defined for this event type"INVALID"
- Event failed signature verification
PayPal - Custom Webhook Validation
For PayPal, implement custom validation with fallback verification:
app.post('/webhook/paypal', async (req, res) => {
try {
// Parse raw body to JSON
let parsedBody;
try {
parsedBody = JSON.parse(req.body.toString());
} catch (parseError) {
console.error('Failed to parse webhook body:', parseError);
return res.status(400).send('Invalid JSON payload');
}
// Validate PayPal webhook structure
const isValidPayPalWebhook = (
parsedBody.id &&
parsedBody.event_type &&
parsedBody.resource &&
parsedBody.create_time &&
(req.headers['paypal-transmission-id'] || req.headers['Paypal-Transmission-Id'])
);
if (!isValidPayPalWebhook) {
return res.status(400).send('Invalid webhook structure');
}
// Process events based on type
const eventType = parsedBody.event_type;
switch (eventType) {
case 'PAYMENT.CAPTURE.COMPLETED':
console.log('✅ Payment completed:', {
transactionId: parsedBody.resource.id,
amount: parsedBody.resource.amount.value,
currency: parsedBody.resource.amount.currency_code
});
res.status(200).send('OK - Payment completed processed');
break;
case 'PAYMENT.CAPTURE.DENIED':
console.log('❌ Payment denied:', parsedBody.resource.id);
res.status(200).send('OK - Payment denied processed');
break;
default:
console.log(`ℹ️ Unhandled event type: ${eventType}`);
res.status(200).send('OK - Event acknowledged');
}
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Webhook processing failed');
}
});
Razorpay - Simplified Webhook Handling
For Razorpay, use the built-in handleWebhookEvent
method that automatically handles signature verification:
import express from 'express';
const app = express();
app.post('/webhook/razorpay', async (req, res) => {
try {
const rawBodyString = req.body.toString();
const result = await razorpay.handleWebhookEvent(rawBodyString, req.headers);
if (result === 'INVALID') {
return res.status(400).send('Invalid webhook signature');
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Webhook processing failed');
}
});
The handleWebhookEvent
method returns:
"VERIFIED"
- Event verified and processed successfully"UNHANDLED"
- Event verified but no handler defined for this event type"INVALID"
- Event failed signature verification
Setup:
- Use
express.raw({ type: "application/json" })
for the webhook route - Configure the webhook URL in the Razorpay Dashboard (e.g.,
https://yourdomain.com/webhook/razorpay
)
Supported Events:
payment.captured
payment.failed
order.paid
refund.processed
Middleware Setup for Webhooks
Configure Express middleware properly for webhook endpoints:
const express = require('express');
const app = express();
// CRITICAL: Raw body parsing for webhooks BEFORE JSON parsing
app.use('/webhook', express.raw({ type: 'application/json' }));
// JSON parsing for other endpoints
app.use(express.json());
Best Practices
Response Codes
- HTTP 200: Successfully processed event
- HTTP 400: Invalid webhook structure or signature verification failed
- HTTP 500: Internal server error during processing
Security
- Always verify webhook signatures/headers before processing
- Use raw body parsing for signature verification
- Log webhook events for debugging in development
- Never process unverified webhook events in production
- Rotate webhook secrets periodically
Error Handling
- Handle parsing errors gracefully
- Log detailed error information for debugging
- Return appropriate HTTP status codes
- Implement idempotency to handle duplicate webhook deliveries
Event Processing
- Process events asynchronously when possible
- Implement proper event handlers for each event type
- Store webhook events for audit trails
- Handle unknown event types gracefully
Development vs Production
- Use more permissive validation in development/sandbox environments
- Implement strict signature verification in production
- Log comprehensive debugging information in development
- Use structured logging in production for monitoring