Stripe Payment Processing
Setup
import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
Checkout Session (simplest integration)
app.post('/api/checkout', async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: 'payment', // or 'subscription'
line_items: [{ price: req.body.priceId, quantity: 1 }],
success_url: ${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID},
cancel_url: ${process.env.APP_URL}/cancel,
metadata: { userId: req.user.id },
});
res.json({ url: session.url });
});
Payment Intents (custom UI)
// Server: create intent
app.post('/api/payment-intent', async (req, res) => {
const intent = await stripe.paymentIntents.create({
amount: Math.round(req.body.amount * 100), // cents
currency: 'usd',
automatic_payment_methods: { enabled: true },
metadata: { orderId: req.body.orderId },
}, { idempotencyKey: order_${req.body.orderId} });
res.json({ clientSecret: intent.client_secret });
});
// Client: React + Stripe.js import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
function CheckoutForm() { const stripe = useStripe(); const elements = useElements();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const { error } = await stripe!.confirmPayment({
elements: elements!,
confirmParams: { return_url: ${window.location.origin}/success },
});
if (error) setError(error.message!);
};
return ( <form onSubmit={handleSubmit}> <PaymentElement /> <button disabled={!stripe}>Pay</button> </form> ); }
Subscriptions
app.post('/api/subscribe', async (req, res) => { const subscription = await stripe.subscriptions.create({ customer: customerId, items: [{ price: req.body.priceId }], payment_behavior: 'default_incomplete', expand: ['latest_invoice.payment_intent'], }); const invoice = subscription.latest_invoice as Stripe.Invoice; const pi = invoice.payment_intent as Stripe.PaymentIntent; res.json({ subscriptionId: subscription.id, clientSecret: pi.client_secret }); });
// Cancel at period end (not immediately) await stripe.subscriptions.update(subId, { cancel_at_period_end: true });
Webhooks (critical)
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => { const event = stripe.webhooks.constructEvent( req.body, req.headers['stripe-signature']!, process.env.STRIPE_WEBHOOK_SECRET! );
switch (event.type) { case 'checkout.session.completed': await fulfillOrder(event.data.object as Stripe.Checkout.Session); break; case 'invoice.paid': await activateSubscription(event.data.object as Stripe.Invoice); break; case 'invoice.payment_failed': await handleFailedPayment(event.data.object as Stripe.Invoice); break; case 'customer.subscription.deleted': await deactivateSubscription(event.data.object as Stripe.Subscription); break; } res.json({ received: true }); });
Critical: Use express.raw() — parsed body breaks signature verification.
Anti-Patterns
Anti-Pattern Fix
Trust client-side amount Calculate amount server-side only
No webhook handling Always use webhooks for fulfillment
Storing card numbers Use Stripe.js — never touch card data
No idempotency keys Add to all create operations
Fulfilling before payment confirms Wait for webhook confirmation
No error handling on client Show error.message from Stripe
Quick Troubleshooting
Issue Fix
Webhook 400 Use raw body, verify STRIPE_WEBHOOK_SECRET
"No such price" Check test/live mode matches API key
Double charges Add idempotency key
CORS on redirect Ensure HTTPS everywhere
Production Checklist
-
Webhook endpoint registered and verified
-
Idempotency keys on all creates
-
Amount calculated server-side
-
Test mode for dev/staging, live for prod
-
PCI compliance (never handle raw card data)
-
Customer portal for subscription management
-
Refund flow implemented