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 →