Stripe Payment Integration Guide
Learn how to integrate Stripe payments with the Unified Commerce Platform for secure, PCI-compliant payment processing. This guide covers setup, implementation, testing, and production deployment.
Prerequisites
Before starting, ensure you have:
- Stripe account (Sign up here)
- Unified Commerce API credentials
- Basic understanding of payment processing concepts
- Node.js environment for backend implementation
Table of Contents
- Stripe Setup
- Backend Integration
- Frontend Implementation
- Payment Methods
- 3D Secure Authentication
- Webhooks
- Refunds & Disputes
- Testing
- Production Checklist
- Troubleshooting
Stripe Setup
Step 1: Get Your Stripe Keys
- Log into Stripe Dashboard
- Navigate to Developers → API keys
- Copy your keys:
- Test Mode:
pk_test_...
andsk_test_...
- Live Mode:
pk_live_...
andsk_live_...
- Test Mode:
Step 2: Configure Unified Commerce
Update your merchant settings via API:
mutation UpdateMerchantSettings($input: UpdateMerchantSettingsInput!) {
updateMerchantSettings(input: $input) {
settings {
payments {
stripeEnabled
stripePublishableKey
stripeSupportedCountries
stripeSupportedCurrencies
}
}
errors {
field
message
}
}
}
# Variables
{
"input": {
"payments": {
"stripeEnabled": true,
"stripePublishableKey": "pk_test_...",
"stripeSecretKey": "sk_test_...",
"stripeWebhookSecret": "whsec_...",
"stripeSupportedCountries": ["US", "CA", "GB", "EU"],
"stripeSupportedCurrencies": ["USD", "CAD", "GBP", "EUR"]
}
}
}
Step 3: Install Dependencies
# Backend dependencies
npm install stripe @types/stripe
# Frontend dependencies
npm install @stripe/stripe-js @stripe/react-stripe-js
Backend Integration
Step 1: Initialize Stripe Client
Create lib/stripe.ts
:
import Stripe from 'stripe';
// Initialize Stripe with your secret key
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
typescript: true,
});
// Helper to convert cents to Stripe amount
export const toStripeAmount = (cents: number): number => {
return Math.round(cents);
};
// Helper to convert Stripe amount to cents
export const fromStripeAmount = (amount: number): number => {
return amount;
};
// Validate Stripe webhook signature
export const validateWebhookSignature = (
payload: string | Buffer,
signature: string,
secret: string
): Stripe.Event => {
try {
return stripe.webhooks.constructEvent(payload, signature, secret);
} catch (err) {
throw new Error(`Webhook signature verification failed: ${err.message}`);
}
};
Step 2: Create Payment Intent
Create api/payments/create-intent.ts
:
import { stripe, toStripeAmount } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';
interface CreatePaymentIntentInput {
orderId: string;
amount: number; // in cents
currency: string;
customerId?: string;
metadata?: Record<string, string>;
}
export async function createPaymentIntent({
orderId,
amount,
currency,
customerId,
metadata = {},
}: CreatePaymentIntentInput) {
try {
// Create payment intent in Stripe
const intent = await stripe.paymentIntents.create({
amount: toStripeAmount(amount),
currency: currency.toLowerCase(),
customer: customerId,
metadata: {
orderId,
merchantId: process.env.UC_MERCHANT_ID!,
...metadata,
},
automatic_payment_methods: {
enabled: true,
},
capture_method: 'automatic', // or 'manual' for auth-only
});
// Record payment intent in Unified Commerce
const { data } = await sdk.createPaymentIntent({
input: {
orderId,
stripeIntentId: intent.id,
amount,
currency,
status: 'PENDING',
}
});
if (data?.createPaymentIntent?.errors?.length) {
throw new Error(data.createPaymentIntent.errors[0].message);
}
return {
clientSecret: intent.client_secret,
intentId: intent.id,
};
} catch (error) {
console.error('Failed to create payment intent:', error);
throw error;
}
}
Step 3: Handle Payment Confirmation
Create api/payments/confirm-payment.ts
:
import { stripe } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';
export async function confirmPayment(
intentId: string,
paymentMethodId: string
) {
try {
// Confirm payment with Stripe
const intent = await stripe.paymentIntents.confirm(intentId, {
payment_method: paymentMethodId,
return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/checkout/success`,
});
// Update order status based on payment result
if (intent.status === 'succeeded') {
const { data } = await sdk.updateOrderPaymentStatus({
stripeIntentId: intentId,
status: 'PAID',
paidAt: new Date().toISOString(),
});
return {
success: true,
orderId: data?.updateOrderPaymentStatus?.order?.id,
};
} else if (intent.status === 'requires_action') {
// 3D Secure authentication required
return {
requiresAction: true,
clientSecret: intent.client_secret,
};
} else {
throw new Error(`Payment failed: ${intent.status}`);
}
} catch (error) {
console.error('Payment confirmation failed:', error);
throw error;
}
}
Step 4: Create Customer Profile
Create api/payments/customer.ts
:
import { stripe } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';
export async function createStripeCustomer(
customerId: string,
email: string,
name?: string
) {
try {
// Create customer in Stripe
const customer = await stripe.customers.create({
email,
name,
metadata: {
customerId,
merchantId: process.env.UC_MERCHANT_ID!,
},
});
// Store Stripe customer ID in Unified Commerce
const { data } = await sdk.updateCustomer({
id: customerId,
input: {
stripeCustomerId: customer.id,
}
});
return customer;
} catch (error) {
console.error('Failed to create Stripe customer:', error);
throw error;
}
}
// Save payment method for future use
export async function savePaymentMethod(
customerId: string,
paymentMethodId: string
) {
try {
// Attach payment method to customer
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
// Set as default payment method
await stripe.customers.update(customerId, {
invoice_settings: {
default_payment_method: paymentMethodId,
},
});
return { success: true };
} catch (error) {
console.error('Failed to save payment method:', error);
throw error;
}
}
Frontend Implementation
Step 1: Setup Stripe Elements
Create components/payment/stripe-provider.tsx
:
'use client';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { ReactNode } from 'react';
// Load Stripe outside of component to avoid recreating on every render
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
interface StripeProviderProps {
children: ReactNode;
options?: {
clientSecret?: string;
appearance?: any;
};
}
export function StripeProvider({ children, options }: StripeProviderProps) {
const defaultOptions = {
appearance: {
theme: 'stripe' as const,
variables: {
colorPrimary: '#3B82F6',
colorBackground: '#ffffff',
colorText: '#1F2937',
colorDanger: '#EF4444',
fontFamily: 'Inter, system-ui, sans-serif',
spacingUnit: '4px',
borderRadius: '8px',
},
rules: {
'.Label': {
fontWeight: '500',
marginBottom: '8px',
},
'.Input': {
padding: '12px',
fontSize: '16px',
},
'.Input:focus': {
borderColor: '#3B82F6',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
},
'.Error': {
fontSize: '14px',
marginTop: '4px',
},
},
},
...options,
};
return (
<Elements stripe={stripePromise} options={defaultOptions}>
{children}
</Elements>
);
}
Step 2: Create Payment Form
Create components/payment/payment-form.tsx
:
'use client';
import { useState } from 'react';
import {
PaymentElement,
useStripe,
useElements,
AddressElement,
} from '@stripe/react-stripe-js';
interface PaymentFormProps {
amount: number; // in cents
currency: string;
onSuccess: (paymentIntent: any) => void;
onError: (error: string) => void;
}
export function PaymentForm({
amount,
currency,
onSuccess,
onError,
}: PaymentFormProps) {
const stripe = useStripe();
const elements = useElements();
const [isProcessing, setIsProcessing] = useState(false);
const [saveCard, setSaveCard] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
return;
}
setIsProcessing(true);
try {
// Confirm payment
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/checkout/success`,
payment_method_data: {
billing_details: {
// Will be collected by AddressElement
},
},
save_payment_method: saveCard,
},
redirect: 'if_required', // Only redirect if necessary (3D Secure)
});
if (error) {
if (error.type === 'card_error' || error.type === 'validation_error') {
onError(error.message || 'Payment failed');
} else {
onError('An unexpected error occurred');
}
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
onSuccess(paymentIntent);
}
} catch (err) {
console.error('Payment error:', err);
onError('Payment processing failed');
} finally {
setIsProcessing(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Billing Address */}
<div>
<h3 className="text-lg font-semibold mb-4">Billing Address</h3>
<AddressElement
options={{
mode: 'billing',
allowedCountries: ['US', 'CA', 'GB'],
fields: {
phone: 'always',
},
validation: {
phone: {
required: 'always',
},
},
}}
/>
</div>
{/* Payment Method */}
<div>
<h3 className="text-lg font-semibold mb-4">Payment Method</h3>
<PaymentElement
options={{
layout: 'tabs',
paymentMethodOrder: ['card', 'apple_pay', 'google_pay'],
fields: {
billingDetails: {
address: 'never', // Collected by AddressElement
},
},
}}
/>
</div>
{/* Save Card Option */}
<div className="flex items-center">
<input
type="checkbox"
id="save-card"
checked={saveCard}
onChange={(e) => setSaveCard(e.target.checked)}
className="mr-2"
/>
<label htmlFor="save-card" className="text-sm">
Save payment method for future purchases
</label>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={!stripe || isProcessing}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isProcessing ? (
<span className="flex items-center justify-center">
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Processing...
</span>
) : (
`Pay ${new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(amount / 100)}`
)}
</button>
</form>
);
}
Step 3: Handle Express Checkout
Create components/payment/express-checkout.tsx
:
'use client';
import { PaymentRequestButtonElement } from '@stripe/react-stripe-js';
import { useStripe } from '@stripe/react-stripe-js';
import { useEffect, useState } from 'react';
interface ExpressCheckoutProps {
amount: number;
currency: string;
onSuccess: (paymentMethod: any) => void;
}
export function ExpressCheckout({
amount,
currency,
onSuccess,
}: ExpressCheckoutProps) {
const stripe = useStripe();
const [paymentRequest, setPaymentRequest] = useState<any>(null);
useEffect(() => {
if (!stripe) return;
const pr = stripe.paymentRequest({
country: 'US',
currency: currency.toLowerCase(),
total: {
label: 'Total',
amount: amount,
},
requestPayerName: true,
requestPayerEmail: true,
requestShipping: true,
shippingOptions: [
{
id: 'standard',
label: 'Standard Shipping',
detail: '5-7 business days',
amount: 500, // $5.00
},
{
id: 'express',
label: 'Express Shipping',
detail: '2-3 business days',
amount: 1500, // $15.00
},
],
});
// Check if Payment Request API is available
pr.canMakePayment().then((result) => {
if (result) {
setPaymentRequest(pr);
}
});
// Handle payment method
pr.on('paymentmethod', async (event) => {
// Process payment
onSuccess(event.paymentMethod);
// Complete payment
event.complete('success');
});
// Handle shipping address change
pr.on('shippingaddresschange', async (event) => {
// Update shipping options based on address
event.updateWith({
status: 'success',
shippingOptions: [
{
id: 'standard',
label: 'Standard Shipping',
detail: '5-7 business days',
amount: 500,
},
],
});
});
}, [stripe, amount, currency]);
if (!paymentRequest) {
return null;
}
return (
<div className="mb-6">
<div className="text-center text-sm text-gray-500 mb-2">
Express checkout
</div>
<PaymentRequestButtonElement
options={{
paymentRequest,
style: {
paymentRequestButton: {
type: 'default',
theme: 'dark',
height: '48px',
},
},
}}
/>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or pay with card</span>
</div>
</div>
</div>
);
}
Payment Methods
Credit/Debit Cards
// Basic card payment
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: customerName,
email: customerEmail,
address: {
line1: address.line1,
city: address.city,
state: address.state,
postal_code: address.postalCode,
country: address.country,
},
},
});
Digital Wallets
// Apple Pay / Google Pay setup
const paymentRequest = stripe.paymentRequest({
country: 'US',
currency: 'usd',
total: {
label: 'Order Total',
amount: 2999, // $29.99
},
requestPayerName: true,
requestPayerEmail: true,
});
// Check availability
const canMakePayment = await paymentRequest.canMakePayment();
if (canMakePayment) {
// Show Apple Pay/Google Pay button
}
Bank Transfers (ACH)
// ACH Direct Debit
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'us_bank_account',
us_bank_account: {
account_holder_type: 'individual',
routing_number: '110000000',
account_number: '000123456789',
},
billing_details: {
name: customerName,
email: customerEmail,
},
});
// Verify micro-deposits
const { error } = await stripe.verifyMicrodeposits(
paymentIntentId,
{ amounts: [32, 45] } // Customer enters amounts
);
Buy Now, Pay Later (BNPL)
// Klarna integration
const { error } = await stripe.confirmKlarnaPayment(
clientSecret,
{
payment_method: {
billing_details: {
address: {
country: 'US',
},
email: customerEmail,
},
},
return_url: `${window.location.origin}/checkout/success`,
}
);
// Afterpay/Clearpay
const { error } = await stripe.confirmAfterpayClearpayPayment(
clientSecret,
{
payment_method: {
billing_details: {
email: customerEmail,
name: customerName,
address: billingAddress,
},
},
return_url: `${window.location.origin}/checkout/success`,
}
);
3D Secure Authentication
Implementation
// Handle 3D Secure authentication
export async function handle3DSecure(
clientSecret: string,
paymentMethodId: string
) {
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: paymentMethodId,
return_url: `${window.location.origin}/checkout/3ds-redirect`,
}
);
if (error) {
if (error.code === 'payment_intent_authentication_failure') {
// Authentication failed
return { error: '3D Secure authentication failed' };
}
return { error: error.message };
}
if (paymentIntent.status === 'succeeded') {
return { success: true, paymentIntent };
} else if (paymentIntent.status === 'requires_action') {
// Additional authentication required
const { error: confirmError } = await stripe.confirmCardPayment(
clientSecret
);
if (confirmError) {
return { error: confirmError.message };
}
return { success: true, paymentIntent };
}
}
3DS Redirect Handler
Create app/checkout/3ds-redirect/page.tsx
:
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { loadStripe } from '@stripe/stripe-js';
export default function ThreeDSRedirect() {
const searchParams = useSearchParams();
const router = useRouter();
const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing');
useEffect(() => {
const handleRedirect = async () => {
const clientSecret = searchParams.get('payment_intent_client_secret');
if (!clientSecret) {
setStatus('error');
return;
}
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
if (!stripe) {
setStatus('error');
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
if (paymentIntent?.status === 'succeeded') {
// Update order status
await fetch('/api/payments/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentIntentId: paymentIntent.id }),
});
setStatus('success');
router.push('/checkout/success');
} else {
setStatus('error');
}
};
handleRedirect();
}, [searchParams, router]);
return (
<div className="min-h-screen flex items-center justify-center">
{status === 'processing' && (
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto" />
<p className="mt-4">Processing your payment...</p>
</div>
)}
{status === 'error' && (
<div className="text-center">
<p className="text-red-600">Payment authentication failed</p>
<a href="/checkout" className="mt-4 text-blue-600 underline">
Return to checkout
</a>
</div>
)}
</div>
);
}
Webhooks
Step 1: Create Webhook Handler
Create app/api/webhooks/stripe/route.ts
:
import { NextRequest, NextResponse } from 'next/server';
import { stripe, validateWebhookSignature } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';
export async function POST(request: NextRequest) {
const payload = await request.text();
const signature = request.headers.get('stripe-signature')!;
try {
// Validate webhook signature
const event = validateWebhookSignature(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
// Handle different event types
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(event.data.object);
break;
case 'charge.refunded':
await handleRefund(event.data.object);
break;
case 'charge.dispute.created':
await handleDispute(event.data.object);
break;
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
case 'invoice.payment_failed':
await handleInvoiceFailure(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 400 }
);
}
}
async function handlePaymentSuccess(paymentIntent: any) {
const orderId = paymentIntent.metadata.orderId;
// Update order status
await sdk.updateOrder({
id: orderId,
input: {
status: 'PROCESSING',
paymentStatus: 'PAID',
paidAt: new Date().toISOString(),
stripePaymentIntentId: paymentIntent.id,
}
});
// Send confirmation email
await sdk.sendOrderConfirmation({ orderId });
// Trigger inventory update
await sdk.reserveInventoryForOrder({ orderId });
}
async function handlePaymentFailure(paymentIntent: any) {
const orderId = paymentIntent.metadata.orderId;
// Update order status
await sdk.updateOrder({
id: orderId,
input: {
status: 'PAYMENT_FAILED',
paymentStatus: 'FAILED',
paymentError: paymentIntent.last_payment_error?.message,
}
});
// Send failure notification
await sdk.sendPaymentFailureNotification({ orderId });
}
async function handleRefund(charge: any) {
const orderId = charge.metadata.orderId;
const refundAmount = charge.amount_refunded;
// Create refund record
await sdk.createRefund({
input: {
orderId,
amount: refundAmount,
reason: charge.refunds.data[0]?.reason || 'requested_by_customer',
stripeRefundId: charge.refunds.data[0]?.id,
status: 'COMPLETED',
}
});
// Update order status
await sdk.updateOrder({
id: orderId,
input: {
status: refundAmount === charge.amount ? 'REFUNDED' : 'PARTIALLY_REFUNDED',
refundedAmount: refundAmount,
}
});
}
async function handleDispute(dispute: any) {
const orderId = dispute.metadata.orderId;
// Create dispute record
await sdk.createDispute({
input: {
orderId,
amount: dispute.amount,
reason: dispute.reason,
status: dispute.status,
stripeDisputeId: dispute.id,
evidence_due_by: dispute.evidence_details.due_by,
}
});
// Notify merchant
await sdk.sendDisputeNotification({
orderId,
disputeReason: dispute.reason,
});
}
Step 2: Configure Webhook Endpoint
In Stripe Dashboard:
- Go to Developers → Webhooks
- Add endpoint:
https://yoursite.com/api/webhooks/stripe
- Select events to listen for:
payment_intent.succeeded
payment_intent.payment_failed
charge.refunded
charge.dispute.created
- Add others as needed
- Copy the webhook signing secret
Refunds & Disputes
Processing Refunds
// Full refund
export async function processFullRefund(paymentIntentId: string) {
try {
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
reason: 'requested_by_customer',
});
return { success: true, refund };
} catch (error) {
console.error('Refund failed:', error);
throw error;
}
}
// Partial refund
export async function processPartialRefund(
paymentIntentId: string,
amount: number
) {
try {
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount: amount, // Amount in cents
reason: 'requested_by_customer',
});
return { success: true, refund };
} catch (error) {
console.error('Partial refund failed:', error);
throw error;
}
}
Handling Disputes
// Submit dispute evidence
export async function submitDisputeEvidence(
disputeId: string,
evidence: {
customer_communication?: string;
receipt?: string;
shipping_documentation?: string;
refund_policy?: string;
[key: string]: any;
}
) {
try {
const dispute = await stripe.disputes.update(disputeId, {
evidence: evidence,
submit: true, // Auto-submit when ready
});
return { success: true, dispute };
} catch (error) {
console.error('Failed to submit dispute evidence:', error);
throw error;
}
}
// Close dispute (accept loss)
export async function closeDispute(disputeId: string) {
try {
const dispute = await stripe.disputes.close(disputeId);
return { success: true, dispute };
} catch (error) {
console.error('Failed to close dispute:', error);
throw error;
}
}
Testing
Test Card Numbers
// Test cards for different scenarios
const TEST_CARDS = {
// Successful payment
SUCCESS: '4242424242424242',
SUCCESS_3DS: '4000002500003155', // Requires 3D Secure
// Declined cards
DECLINED_GENERIC: '4000000000000002',
DECLINED_INSUFFICIENT_FUNDS: '4000000000009995',
DECLINED_LOST_CARD: '4000000000009987',
DECLINED_EXPIRED: '4000000000000069',
// Specific errors
INCORRECT_CVC: '4000000000000127',
PROCESSING_ERROR: '4000000000000119',
INCORRECT_NUMBER: '4242424242424241',
// International cards
CANADA: '4000001240000000',
UK: '4000008260000000',
JAPAN: '4000003920000003',
AUSTRALIA: '4000000360000006',
};
// Test bank accounts (ACH)
const TEST_BANK_ACCOUNTS = {
SUCCESS: {
routing: '110000000',
account: '000123456789',
},
FAIL: {
routing: '110000000',
account: '000111111116', // Will fail verification
},
};
Test Scenarios
// Test helper functions
export async function runPaymentTests() {
const tests = [
{
name: 'Successful payment',
card: TEST_CARDS.SUCCESS,
expectedStatus: 'succeeded',
},
{
name: '3D Secure required',
card: TEST_CARDS.SUCCESS_3DS,
expectedStatus: 'requires_action',
},
{
name: 'Insufficient funds',
card: TEST_CARDS.DECLINED_INSUFFICIENT_FUNDS,
expectedError: 'insufficient_funds',
},
{
name: 'Expired card',
card: TEST_CARDS.DECLINED_EXPIRED,
expectedError: 'expired_card',
},
];
for (const test of tests) {
console.log(`Running test: ${test.name}`);
try {
const result = await testPayment(test.card);
if (test.expectedStatus) {
assert(result.status === test.expectedStatus);
}
if (test.expectedError) {
assert(result.error?.code === test.expectedError);
}
console.log(`✓ ${test.name} passed`);
} catch (error) {
console.error(`✗ ${test.name} failed:`, error);
}
}
}
Webhook Testing
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login to your account
stripe login
# Forward webhooks to local endpoint
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger payment_intent.payment_failed
stripe trigger charge.refunded
Production Checklist
Security
- Use environment variables for all keys
- Enable webhook signature verification
- Implement rate limiting on payment endpoints
- Add CSRF protection
- Use HTTPS everywhere
- Implement PCI compliance measures
- Add fraud detection rules
Configuration
- Set up production Stripe keys
- Configure webhook endpoints
- Enable Stripe Radar for fraud prevention
- Set up email receipts
- Configure statement descriptors
- Set up tax collection (if applicable)
- Configure currency support
Monitoring
- Set up payment failure alerts
- Monitor webhook delivery
- Track conversion rates
- Monitor for duplicate payments
- Set up dispute alerts
- Track refund rates
- Monitor API errors
Implementation
// Production configuration
const PRODUCTION_CONFIG = {
// Enable strict security
requireCVV: true,
require3DSecure: 'recommended', // or 'required'
// Fraud prevention
enableRadar: true,
blockHighRiskPayments: true,
requireEmailVerification: true,
// Payment limits
minimumAmount: 100, // $1.00
maximumAmount: 999999, // $9,999.99
// Retry configuration
maxRetries: 3,
retryDelay: 1000, // ms
// Webhook configuration
webhookTimeout: 20000, // 20 seconds
webhookMaxRetries: 5,
};
Troubleshooting
Common Issues
1. Payment Intent Creation Fails
Problem: "Amount must be at least $0.50" Solution:
// Ensure minimum amount
const amount = Math.max(50, calculatedAmount); // Minimum 50 cents
2. 3D Secure Not Triggering
Problem: 3D Secure not showing for EU cards Solution:
// Force 3D Secure for EU cards
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: 'eur',
payment_method_options: {
card: {
request_three_d_secure: 'any', // or 'automatic'
},
},
});
3. Webhook Signature Verification Fails
Problem: "No signatures found matching the expected signature" Solution:
// Use raw body for signature verification
export const config = {
api: {
bodyParser: false, // Important!
},
};
// Get raw body
const buffer = await request.arrayBuffer();
const rawBody = Buffer.from(buffer);
4. Duplicate Payments
Problem: Customer charged multiple times Solution:
// Implement idempotency
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: 'usd',
}, {
idempotencyKey: `order_${orderId}_${timestamp}`,
});
5. Currency Conversion Issues
Problem: Incorrect currency amounts Solution:
// Handle zero-decimal currencies
const ZERO_DECIMAL_CURRENCIES = ['JPY', 'KRW', 'VND', 'CLP'];
function toStripeAmount(amount: number, currency: string): number {
if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
return Math.round(amount); // Already in smallest unit
}
return Math.round(amount * 100); // Convert to cents
}
Best Practices
- Always validate amounts server-side
- Implement proper error handling and recovery
- Use idempotency keys to prevent duplicate charges
- Store all payment metadata for reconciliation
- Implement webhook retry logic
- Use Stripe's testing tools extensively
- Monitor payment success rates
- Implement proper logging for debugging
- Keep Stripe SDK updated
- Follow PCI compliance guidelines
Next Steps
- Implement subscription payments
- Add support for multiple currencies
- Set up advanced fraud rules
- Implement payment links
- Add support for local payment methods
- Set up Connect for marketplace payments
- Implement Stripe Terminal for in-person payments