Stripe's Three Integration Paths
Stripe offers three distinct integration approaches, each with different trade-offs in customization, development effort, and conversion optimization. Understanding which to use — and when to combine them — determines how quickly you ship and how much revenue you capture.
Payment Links require zero code. Create a payment page in the Stripe dashboard and share the URL. Useful for invoices, freelance projects, and beta testing price points before investing in a full integration.
Checkout redirects users to a Stripe-hosted payment page. It handles SCA/3DS compliance automatically, supports 30+ payment methods, and converts well because it's familiar to users. The integration is a single API call from your server.
Elements embeds Stripe's payment UI components directly in your application for a fully customized, on-domain experience. It requires more development effort but provides complete visual control and keeps users on your domain throughout checkout.
Checkout Integration with Next.js
// pages/api/create-checkout-session.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export default async function handler(req, res) {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price: req.body.priceId,
quantity: 1,
}],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
customer_email: req.body.email,
metadata: {
userId: req.body.userId,
},
});
res.json({ url: session.url });
}On the client, redirect to the session URL after creating it. On the success page, verify the session server-side using the session_id query parameter — never trust client-side success redirects for fulfillment.
Subscription Setup
Subscriptions in Stripe have four key objects: Customer, Product, Price, and Subscription. Create Customers once per user and store the Stripe Customer ID in your database. Create Products and Prices in the Stripe dashboard or via API. When a customer subscribes via Checkout or Elements, Stripe creates the Subscription and handles all recurring billing, retry logic, and dunning automatically.
Always provision access based on Subscription status retrieved server-side, never on client-provided data:
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const isActive = ['active', 'trialing'].includes(subscription.status);
const hasPastDueGrace = subscription.status === 'past_due' &&
subscription.current_period_end > Date.now() / 1000;Webhook Handling and Idempotency
Webhooks are the backbone of subscription lifecycle management. Stripe delivers webhook events for payment success, subscription updates, customer.subscription.deleted, and invoice events. Process them in a dedicated endpoint with signature verification:
// pages/api/webhooks/stripe.ts
import { buffer } from 'micro';
export const config = { api: { bodyParser: false } };
export default async function handler(req, res) {
const sig = req.headers['stripe-signature']!;
const buf = await buffer(req);
let event;
try {
event = stripe.webhooks.constructEvent(buf, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Idempotency: check if already processed
const existing = await db.webhookEvents.findUnique({ where: { stripeEventId: event.id } });
if (existing) return res.json({ received: true });
await db.webhookEvents.create({ data: { stripeEventId: event.id, type: event.type } });
switch (event.type) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
await handleSubscriptionChange(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(event.data.object);
break;
}
res.json({ received: true });
}Idempotency — storing processed event IDs and skipping duplicates — is critical because Stripe retries webhook delivery on failure and you may receive the same event multiple times.
Refunds and Disputes
Initiate refunds programmatically: stripe.refunds.create({ charge: chargeId, reason: 'requested_by_customer' }). For disputes, Stripe notifies you via the charge.dispute.created webhook — respond through the dashboard with evidence within the deadline (typically 7–21 days). Automate evidence submission for recurring dispute types using Stripe's dispute evidence API.
SCA and 3D Secure Compliance
Strong Customer Authentication is required in Europe and increasingly enforced globally. Stripe handles 3DS natively in Checkout and Payment Intents with Elements. For manual card charges, always use Payment Intents with confirm: true and handle requires_action status by redirecting to Stripe's 3DS page:
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: 'usd',
customer: customerId,
payment_method: paymentMethodId,
confirm: true,
return_url: 'https://yoursite.com/payment-complete',
});Stripe Connect for Marketplaces
If you're building a marketplace where multiple vendors receive payments, Stripe Connect handles the complex payout routing. Use Express accounts for most marketplace scenarios — Stripe handles KYC and identity verification while you control the onboarding flow. Charge customers using destination charges or separate charges and transfers depending on your liability model.
Metadata Best Practices
Stripe metadata (up to 50 key-value pairs on any object) is invaluable for reconciliation. Always store your internal user ID, plan name, and campaign source on Customers and Subscriptions. This enables SQL queries against Stripe export data that join cleanly with your application database without requiring API calls for every record.
Stripe is genuinely excellent infrastructure. Used correctly, it eliminates the compliance, fraud detection, and payment reliability burden that would otherwise require a dedicated payments team to manage.