Unified Pay Docs

Getting Started

  • Introduction
  • Getting Started

Payment Providers

  • Stripe
  • PayPal
  • Razorpay

Resources

  • API Reference
  • Best Practices
  • FAQ
DocumentationBest Practices

Best Practices

Security, performance, and implementation best practices

Documentation Team

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
Previous
API Reference
Next
FAQ

Was this helpful?

Help us improve our documentation by providing feedback.