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
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 │
└─────────────────────────────┘
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>
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 });
}
}
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 });
}
}
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);
}
}
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});
});
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);
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
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
});
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;
}
}
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
});
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'
}
}
});
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...');
.env file (in .gitignore):
STRIPE_PUBLIC_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
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 });
}
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 });
}
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 });
});
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
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
}
}
}
}
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
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 });
}
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
- Stripe Documentation
- PCI-DSS Compliance Guide
- OWASP Payment Security
- Stripe Security Best Practices
- OAuth 2.0 for API Security
Next Steps
Building an e-commerce platform with payments?
- Start with Stripe (easiest to integrate)
- Test thoroughly with test cards
- Implement webhooks for async payment confirmation
- Use Payment Intents (not charges)
- Never store raw card data
- 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)