close

DEV Community

abhishek pundir
abhishek pundir

Posted on

Payment Processing in Modern E-Commerce: Payment Gateways & Security

Processing payments seems simple: user enters card → charge → done. In reality, it's one of the most security-critical, regulation-heavy, and complex parts of e-commerce. Get it wrong and you lose customers, money, and trust. Here's what you need to know.


The Payment Processing Landscape

Every year, e-commerce platforms process trillions in transactions. But the infrastructure behind it is surprisingly intricate:

Customer → Merchant (You) → Payment Processor → Payment Gateway 
→ Card Network → Issuing Bank → Customer's Bank
Enter fullscreen mode Exit fullscreen mode

Each step requires authentication, encryption, compliance, and error handling. One failure at any point = no sale.


Why You Can't DIY Payment Processing

Never store credit card data yourself.

Seems obvious, right? Yet every year, companies get hacked because they thought they could handle card data "just this once."

PCI-DSS Compliance

If you store, process, or transmit credit card data, you must comply with PCI-DSS (Payment Card Industry Data Security Standard).

Requirements include:

  • ✅ Data encryption (in transit AND at rest)
  • ✅ Network firewalls and intrusion detection
  • ✅ Secure password management
  • ✅ Regular security testing
  • ✅ Restricted access to cardholder data
  • ✅ Audit logging (every access recorded)
  • ✅ Security incident response plan

The cost? Compliance audits run $10K-100K+ per year. A breach? $200K-millions.

The solution? Use a PCI-compliant payment processor. They handle the liability.


Payment Gateways: How They Work

The Ecosystem

┌──────────────┐
│   Customer   │
│  (Cardholder)│
└────────┬─────┘
         │
┌────────▼──────────────┐
│  Your E-Commerce App  │
└────────┬──────────────┘
         │
┌────────▼──────────────────────┐
│   Payment Processor (Stripe)   │
│  - Tokenization              │
│  - Fraud detection           │
│  - Network routing           │
└────────┬──────────────────────┘
         │
┌────────▼──────────────────┐
│  Card Networks (Visa, MC) │
└────────┬──────────────────┘
         │
┌────────▼────────────────────┐
│  Issuing Bank (Customer's)  │
│  - Approves/Declines       │
│  - Charges account         │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Top Payment Processors

Processor Best For Fees Complexity
Stripe Startups, SaaS, Subscription 2.9% + $0.30 Low
Square Retail, Point-of-Sale 2.6% + $0.10 Low
PayPal B2C, Marketplace 3.49% + $0.49 Medium
Braintree Marketplace, Subscriptions 2.9% + $0.30 Medium
Authorize.net Enterprise, Established 2.5% - 3.5% High

We recommend Stripe for most use cases — excellent documentation, developer-friendly, worldwide coverage.


Implementing Stripe: Step by Step

Step 1: Client-Side Tokenization

Never send raw card data to your server. Use Stripe.js to tokenize on the client:

<!-- HTML -->
<form id="payment-form">
  <div id="card-element"></div>
  <button id="submit-btn">Pay $99.99</button>
</form>

<script src="https://js.stripe.com/v3/"></script>
<script>
  const stripe = Stripe('pk_live_YOUR_PUBLIC_KEY');
  const elements = stripe.elements();
  const cardElement = elements.create('card');
  cardElement.mount('#card-element');

  document.getElementById('payment-form').addEventListener('submit', async (e) => {
    e.preventDefault();

    // Tokenize card on client
    const { token } = await stripe.createToken(cardElement);

    if (token) {
      // Send token (not card!) to your server
      const response = await fetch('/api/charge', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: token.id, amount: 9999 })
      });

      const result = await response.json();
      if (result.success) {
        alert('Payment successful!');
      }
    }
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Key point: Card data never touches your server.

Step 2: Server-Side Charge

Your backend receives only the token, never the card:

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function chargeCard(req: Request, res: Response) {
  const { token, amount, description } = req.body;

  try {
    // Create charge using token
    const charge = await stripe.charges.create({
      amount: amount,  // in cents
      currency: 'usd',
      source: token,
      description: description,
      metadata: {
        orderId: req.body.orderId,
        userId: req.user.id
      }
    });

    if (charge.status === 'succeeded') {
      // Save order to database
      await Order.create({
        userId: req.user.id,
        amount: amount,
        chargeId: charge.id,
        status: 'paid'
      });

      res.json({ success: true, chargeId: charge.id });
    }
  } catch (error) {
    console.error('Charge failed:', error.message);
    res.status(400).json({ success: false, error: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced: Payment Intents (Recommended)

Stripe deprecated simple charges in favor of Payment Intents—more flexible, handles 3D Secure, subscriptions, etc.

async function createPaymentIntent(req: Request, res: Response) {
  const { amount, customerId, orderId } = req.body;

  try {
    // Create payment intent
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: 'usd',
      customer: customerId,
      description: `Order #${orderId}`,
      metadata: { orderId },
      // Require confirmation (3D Secure, etc.)
      confirmation_method: 'manual',
      confirm: false
    });

    // Return client secret to client
    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

Client-side:

async function confirmPayment(clientSecret) {
  const { paymentIntent, error } = await stripe.confirmCardPayment(clientSecret, {
    payment_method: {
      card: cardElement,
      billing_details: { name: 'Jenny Rosen' }
    }
  });

  if (paymentIntent.status === 'succeeded') {
    console.log('Payment successful!');
  } else if (error) {
    console.error('Payment failed:', error.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why Payment Intents?

  • ✅ Handles complex payment methods (digital wallets, bank transfers)
  • ✅ Automatic 3D Secure / 2FA
  • ✅ SCA compliance (Strong Customer Authentication)
  • ✅ Idempotent (retry-safe)

Handling Payment Webhooks

Payments are asynchronous. The customer's bank doesn't approve instantly. Use webhooks:

// Listen for Stripe events
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const event = stripe.webhooks.constructEvent(
    req.body,
    sig,
    process.env.STRIPE_WEBHOOK_SECRET
  );

  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log(`Payment successful: ${paymentIntent.id}`);

      // Update order status
      await Order.updateOne(
        { paymentId: paymentIntent.id },
        { status: 'paid' }
      );

      // Send confirmation email
      await sendConfirmationEmail(paymentIntent.metadata.orderId);
      break;

    case 'payment_intent.payment_failed':
      console.log('Payment failed');
      // Notify customer
      break;

    case 'charge.refunded':
      // Handle refund
      await Order.updateOne(
        { chargeId: event.data.object.id },
        { status: 'refunded' }
      );
      break;
  }

  res.json({received: true});
});
Enter fullscreen mode Exit fullscreen mode

Critical: Always verify webhook signature:

// ✅ CORRECT - Verify signature
const event = stripe.webhooks.constructEvent(body, sig, secret);

// ❌ WRONG - No verification (vulnerability!)
const event = JSON.parse(body);
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

1. HTTPS Everywhere

All communication with payment processors MUST be HTTPS. Stripe enforces this.

// ✅ Correct
const stripe = new Stripe('https://api.stripe.com/...', {
  https: true
});

// ❌ Never HTTP
Enter fullscreen mode Exit fullscreen mode

2. Store Tokens, Not Cards

If you need to charge recurring:

// Create customer and save card
const customer = await stripe.customers.create({
  email: user.email,
  payment_method: token,
  invoice_settings: { default_payment_method: token }
});

// Later: charge without card
await stripe.paymentIntents.create({
  amount: 5000,
  customer: customer.id,
  off_session: true
});
Enter fullscreen mode Exit fullscreen mode

Never store raw card data. Never.

3. Idempotency Keys (Prevent Double-Charges)

Network timeout? Retry? Stripe will see duplicate request.

import crypto from 'crypto';

async function chargeUser(userId, amount) {
  // Create unique idempotency key
  const idempotencyKey = crypto
    .createHash('sha256')
    .update(`${userId}-${amount}-${Date.now()}`)
    .digest('hex');

  try {
    const charge = await stripe.paymentIntents.create(
      {
        amount,
        currency: 'usd',
        customer: userId
      },
      { idempotencyKey }  // Stripe uses this for deduplication
    );

    return charge;
  } catch (error) {
    // Even on error, same key returns same result
    // Safe to retry
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: If network fails mid-request, retry safely without double-charging.

4. Rate Limiting & Fraud Detection

Implement rate limiting on your payment endpoint:

import rateLimit from 'express-rate-limit';

const paymentLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 10,                    // 10 attempts
  message: 'Too many payment attempts, try again later',
  standardHeaders: true,
  legacyHeaders: false
});

app.post('/api/charge', paymentLimiter, async (req, res) => {
  // Handle payment
});
Enter fullscreen mode Exit fullscreen mode

Use Stripe's built-in fraud detection:

const paymentIntent = await stripe.paymentIntents.create({
  amount: 5000,
  currency: 'usd',
  payment_method: paymentMethod,
  // Enable radar (Stripe's fraud detection)
  payment_method_options: {
    card: {
      request_three_d_secure: 'automatic'
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

5. Environment Variables

Never hardcode API keys:

// ✅ Correct
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

// ❌ WRONG - exposes key in git history
const stripe = new Stripe('sk_live_abc123...');
Enter fullscreen mode Exit fullscreen mode

.env file (in .gitignore):

STRIPE_PUBLIC_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Enter fullscreen mode Exit fullscreen mode

6. Input Validation

Always validate amounts, currencies, and metadata:

async function validateAndCharge(req: Request) {
  const { amount, currency, orderId } = req.body;

  // Validate amount
  if (!Number.isInteger(amount) || amount < 50) {
    throw new Error('Invalid amount');
  }

  // Validate currency
  if (!['usd', 'eur', 'gbp'].includes(currency)) {
    throw new Error('Invalid currency');
  }

  // Validate order exists and belongs to user
  const order = await Order.findById(orderId);
  if (!order || order.userId !== req.user.id) {
    throw new Error('Order not found');
  }

  // Validate amount matches order
  if (order.total !== amount) {
    throw new Error('Amount mismatch');
  }

  // Safe to charge
  return await stripe.paymentIntents.create({ amount, currency });
}
Enter fullscreen mode Exit fullscreen mode

Handling Different Payment Methods

Modern e-commerce isn't just credit cards:

async function createPaymentIntent(req: Request, res: Response) {
  const { amount, paymentMethod } = req.body;

  const paymentIntent = await stripe.paymentIntents.create({
    amount: amount,
    currency: 'usd',
    payment_method: paymentMethod,
    // Enable multiple payment methods
    payment_method_types: [
      'card',
      'ideal',           // Netherlands
      'bancontact',      // Belgium
      'giropay',         // Germany
      'eps',             // Austria
      'sofort',          // Europe
      'alipay',          // Asia
      'wechat_pay'       // China
    ]
  });

  res.json({ clientSecret: paymentIntent.client_secret });
}
Enter fullscreen mode Exit fullscreen mode

Handling Disputes & Chargebacks

Customers will dispute charges. Here's how to handle it:

// Listen for chargeback
app.post('/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, sig, secret);

  if (event.type === 'charge.dispute.created') {
    const dispute = event.data.object;

    console.log(`Chargeback: ${dispute.id}`);
    console.log(`Amount: ${dispute.amount}`);
    console.log(`Reason: ${dispute.reason}`);

    // Your response strategy
    const evidence = {
      access_activity_log: 'Logs showing customer accessed account',
      billing_address: 'Delivery confirmed to billing address',
      customer_communication: 'Emails showing customer confirmed purchase',
      customer_email_address: 'customer@example.com',
      customer_name: 'John Doe'
    };

    // Submit evidence
    await stripe.disputes.update(dispute.id, { evidence });
  }

  res.json({ received: true });
});
Enter fullscreen mode Exit fullscreen mode

Tips to prevent chargebacks:

  • ✅ Clear product descriptions
  • ✅ Prominent refund policy
  • ✅ Send confirmation emails
  • ✅ Include shipping tracking
  • ✅ Respond to disputes with evidence

PCI Compliance Checklist

Even with a payment processor, you're responsible for:

  • ✅ HTTPS/TLS encryption
  • ✅ Access control (who can see payment data?)
  • ✅ Regular security audits
  • ✅ Vulnerability scanning
  • ✅ Incident response plan
  • ✅ Employee security training
  • ✅ Audit logging

You DON'T need to comply if:

  • ✅ You never touch raw card data
  • ✅ You use tokenization (Stripe, PayPal, etc.)
  • ✅ You use hosted payment forms

Cost Structure

Typical payment processing costs:

Service Monthly Per-Transaction Notes
Stripe Free 2.9% + $0.30 Lowest for small volumes
Square Free 2.6% + $0.10 Good for retail
PayPal Free 3.49% + $0.49 Higher fees
Authorize.net $25 2.5%-3.5% Better for high volume

Example: 1,000 transactions at $50 average

  • Stripe: $1,460/month
  • Square: $1,310/month
  • Authorize.net: $25 + $1,250 = $1,275/month

At scale, processor choice matters.


Common Mistakes

1. Not Testing Failed Payments

Use Stripe test cards:

4000 0000 0000 0002  → Card declined
4000 0025 0000 3155  → Requires authentication
5200 0282 2222 2210  → Mastercard declined
Enter fullscreen mode Exit fullscreen mode

Test both success and failure paths.

2. Ignoring Webhook Failures

If Stripe webhook fails, order status is wrong. Implement retries:

async function handleWebhookWithRetry(event, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await processWebhook(event);
      return;
    } catch (error) {
      console.log(`Attempt ${i + 1} failed:`, error);
      if (i < maxRetries - 1) {
        await sleep(1000 * Math.pow(2, i)); // exponential backoff
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Hardcoding Production Keys

Use environment variables. Period.

4. Not Handling Currency Correctly

Stripe uses the smallest unit (cents for USD, paise for INR):

// ✅ Correct
const amount = 99.99 * 100;  // 9999 cents

// ❌ Wrong
const amount = 99.99;  // Will charge $0.99
Enter fullscreen mode Exit fullscreen mode

5. Trusting Client-Side Validation Alone

Client validation is for UX. Server validation is for security:

// ✅ Always validate on server
async function charge(req: Request) {
  const { amount } = req.body;

  // Never trust client amount
  const order = await Order.findById(req.body.orderId);
  const actualAmount = order.total;  // Server source of truth

  if (amount !== actualAmount) {
    throw new Error('Amount mismatch - fraud attempt?');
  }

  return stripe.paymentIntents.create({ amount: actualAmount });
}
Enter fullscreen mode Exit fullscreen mode

Building Secure Payment Infrastructure

Payment processing is complex. Many companies choose to not build it themselves.

At Vexio, we handle payment integration for enterprise e-commerce platforms. We:

  • ✅ Integrate with Stripe, PayPal, Square (any processor)
  • ✅ Ensure PCI compliance
  • ✅ Implement fraud detection
  • ✅ Handle disputes and chargebacks
  • ✅ Support multiple currencies and payment methods
  • ✅ Provide secure webhooks and reconciliation
  • ✅ Build custom payment workflows

If you're building an e-commerce platform, let Vexio handle payment infrastructure so you can focus on your business.


Summary: Payment Processing Checklist

Before going live with payments:

  • ✅ Choose a processor (Stripe recommended)
  • ✅ Implement client-side tokenization
  • ✅ Use Payment Intents (not deprecated charges)
  • ✅ Verify webhook signatures
  • ✅ Implement idempotency keys
  • ✅ Add rate limiting
  • ✅ Enable fraud detection
  • ✅ Store environment variables securely
  • ✅ Validate on the server
  • ✅ Test with all payment methods
  • ✅ Document your implementation
  • ✅ Set up monitoring and alerts
  • ✅ Plan for disputes and chargebacks

Resources


Next Steps

Building an e-commerce platform with payments?

  1. Start with Stripe (easiest to integrate)
  2. Test thoroughly with test cards
  3. Implement webhooks for async payment confirmation
  4. Use Payment Intents (not charges)
  5. Never store raw card data
  6. Monitor transactions and disputes

Questions about payment integration? Drop a comment below.


Ready to build secure payment infrastructure?

👉 Explore Vexio's e-commerce solutions →

We specialize in helping companies integrate payments, manage transactions, and build scalable payment systems.

Top comments (0)