Payments & Stripe

Stripe Payments mit Claude Code: TypeScript-Integration 2026

Payment Intents, Checkout Sessions, Subscriptions, Webhooks, Customer Portal und Stripe Elements — alles mit Claude Code in TypeScript implementiert.

Stripe gilt als Goldstandard für Payment-Integration — und Claude Code macht die TypeScript-Implementierung schneller als je zuvor. In diesem Guide zeigen wir dir, wie du mit Claude Code eine vollständige Payment-Architektur aufbaust: von einfachen Payment Intents bis hin zu komplexen Subscription-Flows, Webhooks und dem Customer Portal.

💡 Voraussetzungen: Node.js 20+, TypeScript 5.x, ein Stripe-Account (kostenlos). Alle Beispiele sind produktionsreif und folgen den offiziellen Stripe-Best-Practices für 2026.

1. Stripe Setup & Payment Intents

Der erste Schritt jeder Stripe-Integration: das SDK korrekt initialisieren und Payment Intents erstellen. Claude Code generiert dabei typensichere Wrapper, die Idempotenz, Fehlerbehandlung und Logging von Anfang an mitdenken.

Installation & Konfiguration

SETUP

Abhängigkeiten installieren und TypeScript-Konfiguration anpassen

# Stripe SDK + Typen installieren npm install stripe npm install --save-dev @types/node # .env Datei anlegen STRIPE_SECRET_KEY=sk_live_... STRIPE_PUBLISHABLE_KEY=pk_live_... STRIPE_WEBHOOK_SECRET=whsec_...

Stripe-Client initialisieren

PAYMENT

src/lib/stripe.ts — Singleton-Pattern für den Stripe-Client

import Stripe from 'stripe'; // Stripe-Singleton — einmal initialisieren, überall nutzen const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-12-18.acacia', typescript: true, telemetry: false, // Keine Telemetrie ans Stripe-Dashboard maxNetworkRetries: 3, // Automatische Retry-Logik timeout: 10_000, // 10 Sekunden Timeout }); export default stripe;

Payment Intent erstellen

PAYMENT INTENT

src/payments/createPaymentIntent.ts — Mit Idempotenz-Key

import stripe from '../lib/stripe'; import { randomUUID } from 'crypto'; interface CreatePaymentIntentParams { amount: number; // in Cents (z.B. 1999 = 19,99 €) currency: string; // 'eur', 'usd', etc. customerId?: string; metadata?: Record<string, string>; idempotencyKey?: string; } export async function createPaymentIntent( params: CreatePaymentIntentParams ): Promise<{ clientSecret: string; paymentIntentId: string }> { const { amount, currency, customerId, metadata, idempotencyKey } = params; // Idempotenz-Key verhindert doppelte Abbuchungen bei Netzwerkfehlern const key = idempotencyKey ?? randomUUID(); const intent = await stripe.paymentIntents.create( { amount, currency, customer: customerId, automatic_payment_methods: { enabled: true }, metadata: { ...metadata, created_via: 'agentic-movers-api', }, capture_method: 'automatic', }, { idempotencyKey: key, } ); if (!intent.client_secret) { throw new Error('Kein client_secret vom Stripe-API erhalten'); } return { clientSecret: intent.client_secret, paymentIntentId: intent.id, }; } // Payment Intent bestätigen (serverseitig, ohne 3DS) export async function confirmPaymentIntent(paymentIntentId: string): Promise<boolean> { const intent = await stripe.paymentIntents.confirm(paymentIntentId, { payment_method: 'pm_card_visa', // nur in Tests! }); return intent.status === 'succeeded'; }
💡 Claude Code Prompt: "Erstelle mir eine TypeScript-Funktion für Stripe Payment Intents mit Idempotenz-Key, automatischen Retries und vollständiger Fehlerbehandlung für StripeCardError, StripeInvalidRequestError und StripeAPIError."

API Route (Next.js / Express)

API ROUTE

app/api/payment-intent/route.ts (Next.js App Router)

import { NextRequest, NextResponse } from 'next/server'; import { createPaymentIntent } from '@/payments/createPaymentIntent'; import Stripe from 'stripe'; export async function POST(req: NextRequest): Promise<NextResponse> { try { const { amount, currency = 'eur', customerId } = await req.json(); if (!amount || amount < 50) { return NextResponse.json( { error: 'Mindestbetrag 0,50 €' }, { status: 400 } ); } const result = await createPaymentIntent({ amount, currency, customerId }); return NextResponse.json(result, { status: 201 }); } catch (err) { if (err instanceof Stripe.errors.StripeCardError) { return NextResponse.json( { error: err.message, code: err.code }, { status: 402 } ); } console.error('[PaymentIntent] Unerwarteter Fehler:', err); return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 }); } }

2. Checkout Sessions

Stripe Checkout ist der schnellste Weg zu einem konvertierungsoptimierten Payment-Flow — gehostet von Stripe, vollständig konfigurierbar per API. Claude Code hilft dabei, komplexe Session-Konfigurationen mit line_items, Kundenvorausfüllung und Metadaten sauber zu strukturieren.

CHECKOUT

src/payments/createCheckoutSession.ts — Vollständige Checkout-Session

import stripe from '../lib/stripe'; interface LineItem { priceId: string; // Stripe Price ID (price_xxxxx) quantity: number; } interface CheckoutSessionParams { lineItems: LineItem[]; mode: 'payment' | 'subscription' | 'setup'; successUrl: string; cancelUrl: string; customerId?: string; customerEmail?: string; metadata?: Record<string, string>; allowPromotionCodes?: boolean; trialPeriodDays?: number; } export async function createCheckoutSession( params: CheckoutSessionParams ): Promise<{ url: string; sessionId: string }> { const { lineItems, mode, successUrl, cancelUrl, customerId, customerEmail, metadata, allowPromotionCodes = true, trialPeriodDays, } = params; const session = await stripe.checkout.sessions.create({ mode, line_items: lineItems.map(item => ({ price: item.priceId, quantity: item.quantity, })), // Kundenvorausfüllung — erhöht Conversion-Rate customer: customerId, customer_email: customerId ? undefined : customerEmail, success_url: `${successUrl}?session_id={CHECKOUT_SESSION_ID}`, cancel_url: cancelUrl, allow_promotion_codes: allowPromotionCodes, // Automatische Steuerberechnung (EU VAT) automatic_tax: { enabled: true }, tax_id_collection: { enabled: true }, // Rechnungsadresse für B2B-Kunden billing_address_collection: 'auto', // Subscription-spezifisch: Trial-Periode ...(mode === 'subscription' && trialPeriodDays ? { subscription_data: { trial_period_days: trialPeriodDays, metadata: { ...metadata, trial: 'true' }, }, } : {}), metadata: { ...metadata, checkout_mode: mode, }, // Ablauf nach 30 Minuten expires_at: Math.floor(Date.now() / 1000) + 30 * 60, }); if (!session.url) { throw new Error('Stripe hat keine Checkout-URL zurückgegeben'); } return { url: session.url, sessionId: session.id }; } // Session nach erfolgreichem Checkout abrufen export async function retrieveCheckoutSession(sessionId: string) { return stripe.checkout.sessions.retrieve(sessionId, { expand: ['line_items', 'customer', 'subscription'], }); }
⚠️ Wichtig: Die success_url enthält {CHECKOUT_SESSION_ID} als Platzhalter — Stripe ersetzt diesen automatisch. Nutze die Session-ID auf der Success-Page, um den Kauf zu verifizieren, bevor du das Produkt freischaltest.

Session-Verifizierung auf der Success Page

VERIFY

src/payments/verifyCheckout.ts — Kauf nach Redirect verifizieren

import stripe from '../lib/stripe'; export async function verifyCheckoutSession(sessionId: string) { const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['payment_intent', 'subscription', 'customer'], }); // Nur bei 'complete' freischalten — nie bei 'open' oder 'expired' if (session.status !== 'complete') { throw new Error(`Session-Status: ${session.status} — noch nicht abgeschlossen`); } return { customerId: (session.customer as { id: string })?.id, subscriptionId: (session.subscription as { id: string })?.id, paymentIntentId: (session.payment_intent as { id: string })?.id, amountTotal: session.amount_total, currency: session.currency, metadata: session.metadata, }; }

3. Subscriptions & Billing

Recurring Revenue ist das Herzstück moderner SaaS-Produkte. Stripe Subscriptions handeln Trial-Perioden, Proratierung bei Plan-Wechseln und Kündigungen zuverlässig — wenn man die API korrekt nutzt. Claude Code generiert dabei typsichere Wrapper für alle Subscription-Operationen.

SUBSCRIPTION

src/billing/subscriptions.ts — Create, Upgrade, Downgrade, Cancel

import stripe from '../lib/stripe'; // Neue Subscription erstellen (nach Checkout oder direkt) export async function createSubscription(params: { customerId: string; priceId: string; trialPeriodDays?: number; metadata?: Record<string, string>; }) { const { customerId, priceId, trialPeriodDays, metadata } = params; return stripe.subscriptions.create({ customer: customerId, items: [{ price: priceId }], trial_period_days: trialPeriodDays, // Automatische Rechnungen per E-Mail collection_method: 'charge_automatically', // Payment-Verhalten bei fehlgeschlagener Zahlung payment_settings: { payment_method_types: ['card', 'sepa_debit'], save_default_payment_method: 'on_subscription', }, // Dunning: 4 Versuche über 14 Tage, dann canceln billing_thresholds: null, metadata: { ...metadata, price_id: priceId, }, expand: ['latest_invoice.payment_intent'], }); } // Plan upgraden (sofort mit Proratierung) export async function upgradeSubscription(params: { subscriptionId: string; newPriceId: string; }) { const { subscriptionId, newPriceId } = params; const sub = await stripe.subscriptions.retrieve(subscriptionId); const itemId = sub.items.data[0].id; return stripe.subscriptions.update(subscriptionId, { items: [{ id: itemId, price: newPriceId }], proration_behavior: 'create_prorations', // sofort abrechnen payment_behavior: 'pending_if_incomplete', }); } // Plan downgraden (am Perioden-Ende wechseln) export async function downgradeSubscription(params: { subscriptionId: string; newPriceId: string; }) { const { subscriptionId, newPriceId } = params; const sub = await stripe.subscriptions.retrieve(subscriptionId); const itemId = sub.items.data[0].id; return stripe.subscriptions.update(subscriptionId, { items: [{ id: itemId, price: newPriceId }], proration_behavior: 'none', // kein Guthaben, am Ende wechseln billing_cycle_anchor: 'unchanged', }); } // Kündigung am Perioden-Ende (nicht sofort) export async function cancelSubscriptionAtPeriodEnd(subscriptionId: string) { return stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true, }); } // Sofort-Kündigung (z.B. Betrug, DSGVO-Anfrage) export async function cancelSubscriptionImmediately(subscriptionId: string) { return stripe.subscriptions.cancel(subscriptionId, { invoice_now: true, // letzte Rechnung sofort stellen prorate: true, // anteilig gutschreiben }); } // Kündigung rückgängig machen (Reactivation) export async function reactivateSubscription(subscriptionId: string) { return stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: false, }); }

Subscription-Status abfragen

STATUS

Hilfsfunktionen für Subscription-Status-Checks

type SubscriptionStatus = | 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'paused'; export function isSubscriptionActive(status: SubscriptionStatus): boolean { return ['active', 'trialing'].includes(status); } export async function getCustomerSubscriptions(customerId: string) { const subscriptions = await stripe.subscriptions.list({ customer: customerId, status: 'all', expand: ['data.items.data.price.product'], limit: 10, }); return subscriptions.data.map(sub => ({ id: sub.id, status: sub.status, currentPeriodEnd: new Date(sub.current_period_end * 1000), cancelAtPeriodEnd: sub.cancel_at_period_end, trialEnd: sub.trial_end ? new Date(sub.trial_end * 1000) : null, priceId: sub.items.data[0]?.price.id, isActive: isSubscriptionActive(sub.status as SubscriptionStatus), })); }

4. Webhooks & Event Handling

Webhooks sind das Rückgrat jeder Stripe-Integration. Ohne sie weißt du nicht, wann eine Zahlung wirklich erfolgreich war, wann ein Trial abläuft oder wann eine Subscription gekündigt wird. Claude Code generiert dabei idempotente Event-Handler mit vollständiger Signature-Verifizierung.

⚠️ Kritisch: Webhook-Endpoints MÜSSEN den Raw-Request-Body empfangen, nicht den geparsten JSON-Body. In Next.js bedeutet das: export const config = { api: { bodyParser: false } }
WEBHOOK

app/api/stripe/webhook/route.ts — Vollständiger Webhook-Handler

import { NextRequest, NextResponse } from 'next/server'; import stripe from '@/lib/stripe'; import Stripe from 'stripe'; // WICHTIG: Raw Body für Signature-Verifizierung export const runtime = 'nodejs'; export async function POST(req: NextRequest): Promise<NextResponse> { const body = await req.text(); // RAW body — nicht .json()! const signature = req.headers.get('stripe-signature'); if (!signature) { return NextResponse.json({ error: 'Keine Stripe-Signatur' }, { status: 401 }); } let event: Stripe.Event; try { // constructEvent validiert Signatur + Timestamp (verhindert Replay-Attacks) event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err) { console.error('[Webhook] Signature-Fehler:', err); return NextResponse.json({ error: 'Ungültige Signatur' }, { status: 400 }); } // Idempotenz: Event bereits verarbeitet? (z.B. DB-Check) const alreadyProcessed = await checkEventIdempotency(event.id); if (alreadyProcessed) { return NextResponse.json({ received: true, skipped: true }); } try { switch (event.type) { // Checkout erfolgreich abgeschlossen case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session; await handleCheckoutCompleted(session); break; } // Subscription erstellt (nach Checkout oder API) case 'customer.subscription.created': { const sub = event.data.object as Stripe.Subscription; await handleSubscriptionCreated(sub); break; } // Subscription aktualisiert (Upgrade/Downgrade/Cancel) case 'customer.subscription.updated': { const sub = event.data.object as Stripe.Subscription; await handleSubscriptionUpdated(sub); break; } // Subscription endgültig gekündigt case 'customer.subscription.deleted': { const sub = event.data.object as Stripe.Subscription; await handleSubscriptionDeleted(sub); break; } // Rechnung bezahlt (monatlich/jährlich) case 'invoice.payment_succeeded': { const invoice = event.data.object as Stripe.Invoice; await handleInvoicePaymentSucceeded(invoice); break; } // Zahlung fehlgeschlagen — Dunning starten case 'invoice.payment_failed': { const invoice = event.data.object as Stripe.Invoice; await handleInvoicePaymentFailed(invoice); break; } // Trial läuft in 3 Tagen ab — Reminder senden case 'customer.subscription.trial_will_end': { const sub = event.data.object as Stripe.Subscription; await handleTrialWillEnd(sub); break; } default: console.log(`[Webhook] Unbehandeltes Event: ${event.type}`); } // Event als verarbeitet markieren await markEventAsProcessed(event.id, event.type); return NextResponse.json({ received: true }); } catch (err) { console.error(`[Webhook] Handler-Fehler für ${event.type}:`, err); // 500 → Stripe versucht es erneut (bis zu 24h, exponentielles Backoff) return NextResponse.json({ error: 'Handler fehlgeschlagen' }, { status: 500 }); } }

Fulfillment-Logik

FULFILLMENT

src/webhooks/handlers.ts — Konkrete Event-Handler

import stripe from '../lib/stripe'; import Stripe from 'stripe'; import { db } from '../lib/db'; // deine Datenbank-Abstraktion export async function handleCheckoutCompleted( session: Stripe.Checkout.Session ): Promise<void> { const { customer, subscription, metadata } = session; await db.users.update({ where: { stripeCustomerId: customer as string }, data: { subscriptionId: subscription as string, subscriptionStatus: 'active', plan: metadata?.plan ?? 'starter', activatedAt: new Date(), }, }); console.log(`[Webhook] Checkout abgeschlossen: Customer ${customer}`); } export async function handleSubscriptionUpdated( sub: Stripe.Subscription ): Promise<void> { const priceId = sub.items.data[0]?.price.id; await db.subscriptions.upsert({ where: { stripeSubId: sub.id }, update: { status: sub.status, priceId, cancelAtPeriodEnd: sub.cancel_at_period_end, currentPeriodEnd: new Date(sub.current_period_end * 1000), }, create: { stripeSubId: sub.id, stripeCustomerId: sub.customer as string, status: sub.status, priceId, currentPeriodEnd: new Date(sub.current_period_end * 1000), }, }); } export async function handleInvoicePaymentFailed( invoice: Stripe.Invoice ): Promise<void> { const customerId = invoice.customer as string; const attemptCount = invoice.attempt_count; // Nach 3 Fehlversuchen: Warnung per E-Mail, nach 4: deaktivieren if (attemptCount >= 4) { await db.users.update({ where: { stripeCustomerId: customerId }, data: { subscriptionStatus: 'past_due', accessRevoked: true }, }); } console.warn(`[Webhook] Zahlung fehlgeschlagen: Customer ${customerId}, Versuch ${attemptCount}`); } export async function handleTrialWillEnd( sub: Stripe.Subscription ): Promise<void> { const customerId = sub.customer as string; const trialEnd = new Date((sub.trial_end ?? 0) * 1000); // E-Mail-Erinnerung 3 Tage vor Trial-Ende senden console.log(`[Webhook] Trial endet am ${trialEnd.toISOString()} für Customer ${customerId}`); // await emailService.sendTrialEndingReminder(customerId, trialEnd); } // Idempotenz-Hilfsfunktionen async function checkEventIdempotency(eventId: string): Promise<boolean> { const existing = await db.processedWebhookEvents.findUnique({ where: { stripeEventId: eventId }, }); return !!existing; } async function markEventAsProcessed(eventId: string, eventType: string): Promise<void> { await db.processedWebhookEvents.create({ data: { stripeEventId: eventId, eventType, processedAt: new Date() }, }); }
💡 Claude Code Prompt: "Erstelle eine TypeScript-Tabelle `processed_webhook_events` mit Prisma-Schema für Idempotenz-Tracking. Felder: id, stripeEventId (unique), eventType, processedAt. Füge einen Cleanup-Cron hinzu der Events älter als 30 Tage löscht."

5. Customer Portal & Self-Service

Das Stripe Customer Portal ist ein von Stripe gehostetes Dashboard, in dem deine Kunden selbst ihre Subscriptions verwalten, Rechnungen herunterladen und Zahlungsmethoden aktualisieren können — ohne dass du dafür eigene UI bauen musst.

PORTAL

src/billing/customerPortal.ts — Billing Portal Session erstellen

import stripe from '../lib/stripe'; interface BillingPortalParams { customerId: string; returnUrl: string; // Wohin nach dem Portal zurück flowType?: 'subscription_cancel' | 'payment_method_update' | 'subscription_update'; } export async function createBillingPortalSession( params: BillingPortalParams ): Promise<{ url: string }> { const { customerId, returnUrl, flowType } = params; const sessionParams: Parameters<typeof stripe.billingPortal.sessions.create>[0] = { customer: customerId, return_url: returnUrl, }; // Direkter Flow: Öffnet sofort das Kündigungs- / Update-Formular if (flowType === 'subscription_cancel') { sessionParams.flow_data = { type: 'subscription_cancel', subscription_cancel: { subscription: 'current', // aktive Subscription }, }; } else if (flowType === 'payment_method_update') { sessionParams.flow_data = { type: 'payment_method_update', }; } else if (flowType === 'subscription_update') { sessionParams.flow_data = { type: 'subscription_update', subscription_update: { subscription: 'current', }, }; } const session = await stripe.billingPortal.sessions.create(sessionParams); return { url: session.url }; } // Rechnungshistorie direkt via API abrufen (ohne Portal-Redirect) export async function getInvoiceHistory(customerId: string, limit = 10) { const invoices = await stripe.invoices.list({ customer: customerId, limit, expand: ['data.payment_intent'], }); return invoices.data.map(inv => ({ id: inv.id, number: inv.number, status: inv.status, amount: inv.amount_paid, currency: inv.currency, date: new Date((inv.created) * 1000), pdfUrl: inv.invoice_pdf, hostedUrl: inv.hosted_invoice_url, description: inv.description, })); } // Zahlungsmethoden eines Kunden abrufen export async function getPaymentMethods(customerId: string) { const methods = await stripe.paymentMethods.list({ customer: customerId, type: 'card', }); return methods.data.map(pm => ({ id: pm.id, brand: pm.card?.brand, last4: pm.card?.last4, expMonth: pm.card?.exp_month, expYear: pm.card?.exp_year, isDefault: false, // aus Customer.invoice_settings.default_payment_method })); }

API Route für das Portal

API

app/api/billing/portal/route.ts — Geschützte Route mit Auth

import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth/next'; import { createBillingPortalSession } from '@/billing/customerPortal'; export async function POST(req: NextRequest): Promise<NextResponse> { const session = await getServerSession(); if (!session?.user) { return NextResponse.json({ error: 'Nicht eingeloggt' }, { status: 401 }); } const { flowType } = await req.json(); // stripeCustomerId aus Session / DB holen const customerId = session.user.stripeCustomerId; if (!customerId) { return NextResponse.json( { error: 'Kein Stripe-Kunde gefunden' }, { status: 404 } ); } const { url } = await createBillingPortalSession({ customerId, returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`, flowType, }); return NextResponse.json({ url }); } // Rechnungshistorie abrufen export async function GET(req: NextRequest): Promise<NextResponse> { const session = await getServerSession(); if (!session?.user?.stripeCustomerId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { getInvoiceHistory } = await import('@/billing/customerPortal'); const invoices = await getInvoiceHistory(session.user.stripeCustomerId); return NextResponse.json({ invoices }); }
💡 Portal konfigurieren: Im Stripe Dashboard unter Settings → Billing → Customer Portal kannst du festlegen, welche Aktionen Kunden selbst durchführen dürfen: Plan-Wechsel, Kündigung, Zahlungsmethode aktualisieren. Claude Code kann dir dabei helfen, die Konfiguration per API zu setzen.

6. Stripe Elements & Frontend

Stripe Elements ermöglicht dir, eine vollständig angepasste Payment-UI direkt in deine React-App zu integrieren — ohne Redirect auf externe Seiten. Mit dem PaymentElement und den React Hooks von Stripe ist die Integration in 2026 so einfach wie nie zuvor.

Stripe Provider Setup

FRONTEND

src/components/StripeProvider.tsx — Elements-Provider mit Appearance

'use client'; import { Elements } from '@stripe/react-stripe-js'; import { loadStripe, Stripe, StripeElementsOptions } from '@stripe/stripe-js'; import { useMemo, ReactNode } from 'react'; // loadStripe nur einmal aufrufen — außerhalb der Komponente const stripePromise: Promise<Stripe | null> = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! ); interface StripeProviderProps { clientSecret: string; children: ReactNode; } export function StripeProvider({ clientSecret, children }: StripeProviderProps) { const options: StripeElementsOptions = useMemo(() => ({ clientSecret, appearance: { theme: 'stripe', variables: { colorPrimary: '#6366f1', // Indigo — Brand-Farbe anpassen colorBackground: '#ffffff', colorText: '#1e293b', colorDanger: '#ef4444', fontFamily: 'Inter, system-ui, sans-serif', borderRadius: '8px', spacingUnit: '4px', }, rules: { '.Input': { border: '1px solid #e2e8f0', boxShadow: 'none', padding: '12px 16px', }, '.Input:focus': { border: '1px solid #6366f1', boxShadow: '0 0 0 3px rgba(99,102,241,0.15)', }, '.Label': { fontSize: '0.875rem', fontWeight: '500', marginBottom: '6px', }, }, }, locale: 'de', // Stripe UI auf Deutsch }), [clientSecret]); return ( <Elements stripe={stripePromise} options={options}> {children} </Elements> ); }

PaymentForm Komponente

REACT

src/components/PaymentForm.tsx — useStripe + useElements + confirmPayment

'use client'; import { PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'; import { useState, FormEvent } from 'react'; interface PaymentFormProps { amount: number; onSuccess: (paymentIntentId: string) => void; onError?: (error: string) => void; } export function PaymentForm({ amount, onSuccess, onError }: PaymentFormProps) { const stripe = useStripe(); const elements = useElements(); const [isLoading, setIsLoading] = useState(false); const [errorMsg, setErrorMsg] = useState<string | null>(null); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (!stripe || !elements) { return; // Stripe noch nicht geladen } setIsLoading(true); setErrorMsg(null); try { // Formular-Validierung durch Stripe Elements const { error: submitError } = await elements.submit(); if (submitError) { setErrorMsg(submitError.message ?? 'Validierungsfehler'); return; } // Payment bestätigen — leitet ggf. für 3DS weiter const { error, paymentIntent } = await stripe.confirmPayment({ elements, confirmParams: { return_url: `${window.location.origin}/checkout/success`, payment_method_data: { billing_details: { // Aus deinem User-Profil vorausfüllen email: 'user@example.com', }, }, }, redirect: 'if_required', // nur wenn 3DS nötig }); if (error) { const msg = error.type === 'card_error' ? error.message ?? 'Kartenfehler' : 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.'; setErrorMsg(msg); onError?.(msg); } else if (paymentIntent?.status === 'succeeded') { onSuccess(paymentIntent.id); } } finally { setIsLoading(false); } }; return ( <form onSubmit={handleSubmit} style={{ maxWidth: '480px' }}> <PaymentElement options={{ layout: 'tabs', // 'tabs' oder 'accordion' wallets: { applePay: 'auto', googlePay: 'auto' }, }} /> {errorMsg && ( <div style={{ color: '#ef4444', marginTop: '12px', fontSize: '0.875rem' }}> {errorMsg} </div> )} <button type="submit" disabled={isLoading || !stripe || !elements} style={{ marginTop: '20px', width: '100%', padding: '14px', background: '#6366f1', color: 'white', border: 'none', borderRadius: '8px', fontSize: '1rem', fontWeight: '600', cursor: isLoading ? 'not-allowed' : 'pointer', opacity: isLoading ? 0.7 : 1, }} > {isLoading ? 'Zahlung wird verarbeitet...' : `${(amount / 100).toFixed(2).replace('.', ',')} € bezahlen`} </button> </form> ); }

Vollständige Checkout-Page

PAGE

app/checkout/page.tsx — Server-seitiger clientSecret + Client-Komponente

// Server Component — holt clientSecret import { createPaymentIntent } from '@/payments/createPaymentIntent'; import { StripeProvider } from '@/components/StripeProvider'; import { PaymentForm } from '@/components/PaymentForm'; export default async function CheckoutPage() { // Server-seitig — kein API-Call vom Client nötig const { clientSecret } = await createPaymentIntent({ amount: 1999, // 19,99 € currency: 'eur', }); return ( <main style={{ maxWidth: '520px', margin: '80px auto', padding: '0 20px' }}> <h1>Zahlung abschließen</h1> <p style={{ color: '#64748b' }}>Claude Code Mastery — Starter Plan</p> <StripeProvider clientSecret={clientSecret}> <PaymentForm amount={1999} onSuccess={(id) => console.log('Bezahlt!', id)} /> </StripeProvider> </main> ); }
💡 Apple Pay & Google Pay: Mit wallets: { applePay: 'auto', googlePay: 'auto' } werden digitale Wallets automatisch angezeigt, wenn der Browser sie unterstützt. Stripe übernimmt die Domain-Registrierung für Apple Pay automatisch.

Zusammenfassung: Claude Code Prompts für Stripe

PROMPTS

Die effektivsten Claude Code Prompts für Stripe-Integration

  • Setup: "Initialisiere Stripe SDK in TypeScript mit Singleton-Pattern, automatischen Retries, 10s Timeout und vollständiger Fehlertypisierung"
  • Payment Intent: "Erstelle createPaymentIntent mit Idempotenz-Key, StripeCardError-Handling, Logging und Unit-Tests"
  • Webhook: "Generiere einen Next.js Webhook-Handler für alle subscription.*-Events mit Idempotenz-Tabelle in Prisma"
  • Portal: "Erstelle eine React-Komponente für Billing Settings mit Rechnungshistorie, Zahlungsmethoden und Portal-Link"
  • Testing: "Schreibe Jest-Tests für alle Stripe-Webhook-Handler mit Mock-Events aus stripe.webhooks.generateTestHeaderString"
  • Sicherheit: "Audit meinen Stripe-Webhook-Handler auf Replay-Attacks, fehlende Signatur-Verifizierung und SQL-Injection in den Metadaten"

Stripe CLI: Lokales Webhook-Testing

CLI TESTING

Webhooks lokal testen ohne öffentliche URL

# Stripe CLI installieren (macOS) brew install stripe/stripe-cli/stripe # Login stripe login # Lokalen Server mit Stripe verbinden stripe listen --forward-to localhost:3000/api/stripe/webhook # Test-Events triggern stripe trigger checkout.session.completed stripe trigger customer.subscription.created stripe trigger invoice.payment_failed # Spezifisches Event mit Custom-Payload stripe trigger payment_intent.succeeded \ --add payment_intent:metadata[plan]=pro \ --add payment_intent:metadata[user_id]=usr_123 # Webhook-Secret für lokale Tests auslesen stripe listen --print-secret
⚠️ Produktions-Checkliste: Vor dem Go-Live — Webhook-Secret aus Stripe Dashboard (nicht CLI), STRIPE_SECRET_KEY auf Live-Key wechseln, Publishable Key aktualisieren, Webhook-Endpoint in Stripe Dashboard registrieren, alle Event-Typen aktivieren die du handeln willst.

Payment-Modul im Kurs

Im Claude Code Mastery Kurs: vollständiges Stripe-Modul mit Checkout, Subscriptions, Webhooks, Customer Portal und Stripe Elements für TypeScript-Anwendungen.

14 Tage kostenlos testen →