Stripe processes hundreds of billions of dollars annually and powers payments for millions of businesses. Whether you're building a SaaS platform, marketplace, or e-commerce store, this guide walks through every step from initial setup to a battle-tested production deployment.
We'll cover the modern Payment Intents API (not the legacy Charges API), subscription billing with usage-based pricing, webhook reliability patterns, SCA/3D Secure compliance for European customers, and PCI compliance strategies that keep you out of audit scope.
Stripe in 2026: What's Changed
Stripe has expanded far beyond simple card processing. The 2026 platform includes:
- Payment Intents API — The standard for accepting payments. Handles SCA, 3DS, retries, and multi-step flows automatically.
- Stripe Billing — Subscription management with metered billing, usage-based pricing, trials, and dunning.
- Stripe Connect — Multi-party payments for marketplaces and platforms with onboarding, payouts, and 1099 tax forms.
- Stripe Elements — Pre-built, PCI-compliant UI components for payment forms. Handles card validation, error states, and localization.
- Link — One-click checkout for returning Stripe customers. Increases conversion rates by 7-10%.
Step 1: Setup & Configuration
Install the Stripe SDK and configure your server-side and client-side keys:
# Install server and client SDKs npm install stripe @stripe/stripe-js @stripe/react-stripe-js # .env.local STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
// src/lib/stripe.ts — Server-side Stripe instance
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-12-18.acacia', // Always pin the API version
typescript: true,
});
// src/lib/stripe-client.ts — Client-side
import { loadStripe } from '@stripe/stripe-js';
export const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);Critical: Pin Your API Version
Always specify an apiVersion when creating the Stripe instance. Without pinning, Stripe may roll you forward to a new API version with breaking changes. Pin to the version you tested against and upgrade deliberately.
Step 2: Payment Intents (One-Time Payments)
The Payment Intents API is the modern way to accept payments. It handles the entire payment lifecycle: creation, authentication (3DS), capture, and confirmation. Here's the server-client flow:
// Server: app/api/payments/create-intent/route.ts
import { stripe } from '@/lib/stripe';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { amount, currency = 'usd', metadata } = await req.json();
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Stripe uses cents
currency,
metadata: {
order_id: metadata.orderId,
user_id: metadata.userId,
},
automatic_payment_methods: { enabled: true },
});
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
});
}
// Client: components/CheckoutForm.tsx
'use client';
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
export function CheckoutForm({ clientSecret }: { clientSecret: string }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/payment/success`,
},
});
if (error) {
setError(error.message ?? 'Payment failed');
setProcessing(false);
}
// If successful, Stripe redirects to return_url
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button disabled={!stripe || processing}>
{processing ? 'Processing...' : 'Pay Now'}
</button>
{error && <p>{error}</p>}
</form>
);
}Step 3: Subscription Billing
Stripe Billing handles recurring payments, trials, proration, and dunning (failed payment recovery). Here's how to set up a subscription with a free trial:
// Create a subscription with trial
async function createSubscription(customerId: string, priceId: string) {
// Ensure customer has a payment method
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
trial_period_days: 14,
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription',
},
expand: ['latest_invoice.payment_intent'],
});
return {
subscriptionId: subscription.id,
clientSecret: (subscription.latest_invoice as any)
?.payment_intent?.client_secret,
};
}
// Usage-based pricing (metered billing)
async function reportUsage(subscriptionItemId: string, quantity: number) {
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment',
});
}
// Handle subscription changes (upgrade/downgrade)
async function changeSubscription(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await stripe.subscriptions.update(subscriptionId, {
items: [{
id: subscription.items.data[0].id,
price: newPriceId,
}],
proration_behavior: 'create_prorations',
});
}Step 4: Webhooks (Event-Driven Architecture)
Webhooks are the backbone of a reliable Stripe integration. Never rely solely on client-side confirmation—always verify payment status via webhooks. Stripe retries failed webhook deliveries for up to 72 hours.
// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = (await headers()).get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body, signature, process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed');
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
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 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
case 'invoice.payment_failed':
await handleInvoicePaymentFailed(event.data.object);
break;
default:
console.log('Unhandled event type:', event.type);
}
return NextResponse.json({ received: true });
}Webhook Reliability Pattern: Idempotency
Stripe may deliver the same webhook event multiple times. Always make your webhook handlers idempotent—store the event.id in your database and skip processing if you've already handled it. This prevents duplicate order fulfillment, double emails, and data corruption.
Step 5: SCA & 3D Secure Compliance
Strong Customer Authentication (SCA) is required for European payments under PSD2. 3D Secure adds an authentication step where the customer verifies their identity through their bank. The Payment Intents API handles this automatically when you use automatic_payment_methods.
Automatic 3DS
Stripe's Radar determines when 3DS is required based on the card, bank, and regulatory requirements. No additional code needed when using Payment Intents.
Off-Session Payments
For recurring charges or saved cards, set setup_future_usage: "off_session" during the initial payment. This flags the payment method for future use and requests exemptions from 3DS when possible.
Exemptions
Low-value transactions (under 30 EUR), recurring fixed-amount charges, and trusted beneficiary merchants can be exempt from SCA. Stripe requests these exemptions automatically.
Step 6: PCI Compliance
PCI DSS compliance is mandatory for any business that processes, stores, or transmits cardholder data. Stripe simplifies this dramatically by handling card data on their servers. Here's how to stay PCI-compliant:
Use Stripe Elements or Checkout
Card data never touches your servers. Stripe Elements renders a secure iframe for card input. This qualifies you for SAQ A (the simplest PCI questionnaire with only 22 questions).
Never Log Card Data
Never log, store, or transmit raw card numbers, CVVs, or expiry dates. Use Stripe's tokenization—you only handle PaymentMethod IDs and Payment Intent IDs.
Use HTTPS Everywhere
Your entire site must use TLS/HTTPS. Stripe.js refuses to load on non-HTTPS pages. Enforce HSTS headers and redirect all HTTP to HTTPS.
Content Security Policy
Set CSP headers to allow Stripe's domains (js.stripe.com, api.stripe.com). Block inline scripts and restrict third-party access to minimize XSS attack surface.
Step 7: Testing Strategies
Stripe provides a comprehensive test environment. Here are the test card numbers and strategies you need:
// Test card numbers
4242424242424242 // Succeeds (Visa)
4000002500003155 // Requires 3DS authentication
4000000000009995 // Declined (insufficient funds)
4000000000000069 // Declined (expired card)
4000003720000278 // 3DS required, authentication succeeds
// Stripe CLI for local webhook testing
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
// Integration test example
describe('Payment Flow', () => {
it('creates a payment intent', async () => {
const intent = await stripe.paymentIntents.create({
amount: 2000, currency: 'usd',
automatic_payment_methods: { enabled: true },
});
expect(intent.status).toBe('requires_payment_method');
expect(intent.amount).toBe(2000);
});
it('handles webhook signature verification', async () => {
const payload = JSON.stringify({ type: 'payment_intent.succeeded' });
const signature = stripe.webhooks.generateTestHeaderString({
payload, secret: process.env.STRIPE_WEBHOOK_SECRET!,
});
// Verify signature processing works correctly
});
});Why Teams Choose Codazz for Payment Integration
At Codazz, we've built payment systems processing millions of dollars for SaaS platforms, marketplaces, and fintech startups. We handle the full stack—from Stripe integration and subscription billing to Stripe Connect marketplace payouts and PCI compliance.
Battle-Tested
Payment systems that handle edge cases: currency conversions, partial refunds, proration, tax calculations, and failed payment recovery.
PCI Compliant
We build payment flows that keep you at SAQ A compliance level, minimizing audit scope and security risk.
Rapid Delivery
Full Stripe integration including subscriptions, webhooks, and admin dashboards in 4-6 weeks.
Frequently Asked Questions
How much does Stripe charge per transaction?
Stripe charges 2.9% + $0.30 per successful card charge in the US. International cards add 1.5%, and currency conversion adds 1%. Volume discounts are available for businesses processing $1M+/year. There are no setup fees, monthly fees, or hidden charges.
Should I use Stripe Checkout or build a custom payment form?
Use Stripe Checkout for faster time-to-market—it's a hosted payment page with built-in address collection, tax calculation, and mobile optimization. Build a custom form with Stripe Elements when you need full control over the UI, want the payment form embedded in your app, or need a multi-step checkout flow.
How do I handle failed subscription payments?
Configure Stripe's Smart Retries to automatically retry failed payments with optimized timing. Set up dunning emails via Stripe Billing or your own email system. After 3-4 failed attempts, decide whether to cancel the subscription, pause it, or restrict access. Always listen for the invoice.payment_failed webhook.
Is Stripe Connect suitable for my marketplace?
Yes, if you need to split payments between multiple parties. Stripe Connect handles seller onboarding (KYC/AML), payment splitting, payouts to connected accounts, and 1099 tax reporting. Choose between Standard (Stripe-hosted onboarding), Express (simplified onboarding), or Custom (full control) account types.
How do I test Stripe webhooks locally?
Install the Stripe CLI and run "stripe listen --forward-to localhost:3000/api/webhooks/stripe". This proxies live test events to your local server. You can also trigger specific events with "stripe trigger payment_intent.succeeded". Always test edge cases: failed payments, 3DS challenges, subscription renewals, and refunds.
Need a Production-Ready Payment System?
Get a free consultation on your payment integration. We'll architect a Stripe solution tailored to your business model—subscriptions, marketplace payouts, or one-time payments.
Build Your Payment System with Codazz