Stripe Integration Expert
Purpose
Provide comprehensive, accurate guidance for integrating Stripe payment infrastructure based on 3,253+ official Stripe documentation files. Cover all aspects of payment processing, billing, subscriptions, webhooks, fraud prevention, and advanced features.
Documentation Coverage
Full access to official Stripe documentation (when available):
- Location:
docs/stripe/ - Files: 3,253 markdown files
- Coverage: Complete API reference, guides, integrations, and best practices
Note: Documentation must be pulled separately:
pipx install docpull
docpull https://docs.stripe.com -o .claude/skills/stripe/docs
Major Areas:
- Payment processing (Payment Intents, Checkout, Elements)
- Subscription billing (Subscriptions, Invoices, Metering)
- Customer management
- Payment methods (cards, wallets, bank transfers, buy now pay later)
- Webhooks and events
- Connect (marketplace and platform payments)
- Terminal (in-person payments)
- Radar (fraud detection)
- Identity verification
- Tax calculation
- Climate carbon removal
- Issuing (card creation)
- Treasury (banking-as-a-service)
- Financial Connections
- Agentic Commerce (AI agent payments)
When to Use
Invoke when user mentions:
- Payment Processing: Stripe, payments, checkout, payment intent, one-time payment
- Subscriptions: billing, recurring payments, subscription, metered billing, usage-based
- Customer Management: customers, payment methods, invoices
- Webhooks: webhooks, events, payment confirmation, notifications
- Advanced Features: Connect, marketplace, platform, split payments, ACH, SEPA
- Fraud & Security: Radar, 3D Secure, SCA, PCI compliance, fraud detection
- Tax: tax calculation, VAT, GST, sales tax
- In-Person: Terminal, card readers, point of sale
- Identity: verification, KYC, identity checks
- Banking: Treasury, financial accounts, payouts, bank accounts
How to Use Documentation
When answering questions:
Search for specific topics:
# Use Grep to find relevant docs grep -r "payment intent" docs/stripe/ --include="*.md"Read specific API references:
# API docs are in docs/stripe/api/ cat docs/stripe/api/payment_intents/api-payment_intents-create.mdFind integration guides:
# Guides and tutorials ls docs/stripe/*.md
Core Authentication
API Keys
// Server-side (Node.js)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Client-side (safe for browsers)
const stripe = Stripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
Key Types:
- Test:
pk_test_...(publishable),sk_test_...(secret) - Live:
pk_live_...(publishable),sk_live_...(secret) - Restricted: Custom permissions for limited access
Security:
- NEVER commit secret keys
- Use environment variables
- Rotate immediately if exposed
- Use restricted keys when possible
Payment Integration Patterns
Pattern 1: Stripe Checkout (Fastest)
Best for: Quick setup, standard checkout flow, subscriptions
// Server-side API route
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price: 'price_xxx', // Pre-created price ID
quantity: 1,
}],
mode: 'payment', // or 'subscription' or 'setup'
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cancel`,
});
return Response.json({ url: session.url });
}
// Client-side redirect
window.location.href = checkoutUrl;
Pattern 2: Payment Element (Custom UI)
Best for: Custom checkout experience, embedded in your site
// Server: Create Payment Intent
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000, // $20.00 in cents
currency: 'usd',
automatic_payment_methods: { enabled: true },
});
// Client: Mount Payment Element
import { Elements, PaymentElement } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
function CheckoutForm() {
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<PaymentElement />
<button
</Elements>
);
}
Pattern 3: Subscriptions
Create subscription:
const subscription = await stripe.subscriptions.create({
customer: 'cus_xxx',
items: [{ price: 'price_xxx' }],
payment_behavior: 'default_incomplete',
payment_settings: { save_default_payment_method: 'on_subscription' },
expand: ['latest_invoice.payment_intent'],
});
// Return client secret for 3DS
const clientSecret = subscription.latest_invoice.payment_intent.client_secret;
Subscription lifecycle:
incomplete→active→past_due→canceled/unpaid
Metered billing:
// Report usage
await stripe.subscriptionItems.createUsageRecord('si_xxx', {
quantity: 100,
timestamp: Math.floor(Date.now() / 1000),
});
Webhook Implementation
Setup Webhook Endpoint
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
// Verify signature (CRITICAL for security)
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent;
// Fulfill order, send confirmation email
await fulfillOrder(paymentIntent);
break;
case 'payment_intent.payment_failed':
// Notify customer of failure
await notifyPaymentFailure(event.data.object);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
// Update subscription status in database
await updateSubscription(event.data.object);
break;
case 'customer.subscription.deleted':
// Cancel user access
await cancelAccess(event.data.object);
break;
case 'invoice.payment_succeeded':
// Subscription payment succeeded
await confirmSubscriptionPayment(event.data.object);
break;
case 'invoice.payment_failed':
// Notify customer to update payment method
await notifyPaymentMethodUpdate(event.data.object);
break;
case 'charge.dispute.created':
// Handle dispute
await handleDispute(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return new Response(JSON.stringify({ received: true }));
}
Critical webhook events:
payment_intent.succeeded- Payment completedpayment_intent.payment_failed- Payment failedcustomer.subscription.*- Subscription lifecycleinvoice.payment_succeeded- Recurring payment succeededinvoice.payment_failed- Recurring payment failedcharge.dispute.created- Customer disputed chargecheckout.session.completed- Checkout session completed
Testing webhooks locally:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/webhooks
# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
Customer Management
// Create customer
const customer = await stripe.customers.create({
email: 'customer@example.com',
name: 'John Doe',
metadata: { user_id: '12345' },
payment_method: 'pm_xxx',
invoice_settings: {
default_payment_method: 'pm_xxx',
},
});
// Attach payment method
await stripe.paymentMethods.attach('pm_xxx', {
customer: 'cus_xxx',
});
// List customer's payment methods
const paymentMethods = await stripe.paymentMethods.list({
customer: 'cus_xxx',
type: 'card',
});
// Update customer
await stripe.customers.update('cus_xxx', {
metadata: { plan: 'premium' },
});
Advanced Features
Connect (Marketplaces & Platforms)
Create connected account:
const account = await stripe.accounts.create({
type: 'express',
country: 'US',
email: 'vendor@example.com',
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
});
// Create account link for onboarding
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: 'https://example.com/reauth',
return_url: 'https://example.com/return',
type: 'account_onboarding',
});
Split payments:
// Application fee
await stripe.paymentIntents.create({
amount: 10000,
currency: 'usd',
application_fee_amount: 1000, // 10% platform fee
transfer_data: {
destination: 'acct_xxx', // Connected account
},
});
// Or use separate transfers
await stripe.transfers.create({
amount: 9000,
currency: 'usd',
destination: 'acct_xxx',
});
Radar (Fraud Prevention)
// Rules are configured in Dashboard
// But you can review risk scores:
const paymentIntent = await stripe.paymentIntents.retrieve('pi_xxx', {
expand: ['latest_charge.payment_method_details.card'],
});
const riskScore = paymentIntent.latest_charge?.outcome?.risk_score;
const riskLevel = paymentIntent.latest_charge?.outcome?.risk_level; // 'normal', 'elevated', 'highest'
// Block high-risk payments
if (riskLevel === 'highest') {
await stripe.paymentIntents.cancel('pi_xxx');
}
Tax Calculation
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{
price: 'price_xxx',
quantity: 1,
}],
automatic_tax: { enabled: true }, // Stripe calculates tax automatically
customer_update: {
address: 'auto', // Collect address for tax
},
success_url: 'https://example.com/success',
cancel_url: 'https://example.com/cancel',
});
Testing
Test Card Numbers
Success:
4242 4242 4242 4242- Visa5555 5555 5555 4444- Mastercard3782 822463 10005- American Express
Requires 3D Secure:
4000 0025 0000 3155- Visa (3DS required)4000 0027 6000 3184- Visa (3DS required, decline)
Declined:
4000 0000 0000 9995- Generic decline4000 0000 0000 9987- Insufficient funds4000 0000 0000 9979- Stolen card
Other:
- Any future expiry date (e.g.,
12/34) - Any 3-digit CVC (4 digits for Amex)
- Any 5-digit ZIP
Security Best Practices
API Keys:
- Never expose secret keys client-side
- Use restricted keys when possible
- Rotate keys periodically
- Monitor key usage in Dashboard
Webhooks:
- ALWAYS verify webhook signatures
- Use HTTPS endpoints only
- Return 200 immediately, process async
- Handle retries (Stripe retries failed webhooks)
Amounts:
- Validate amounts server-side
- Never trust client-sent amounts
- Use Price objects when possible
PCI Compliance:
- Never store card numbers
- Use Stripe Elements/Checkout
- Let Stripe handle sensitive data
Idempotency:
- Use idempotency keys for critical operations
- Prevents duplicate charges on retry
Common Errors
Authentication:
Invalid API Key- Check key format and environmentNo such customer- Customer deleted or wrong ID
Payment failures:
card_declined- Generic decline (ask customer to try different card)insufficient_funds- Not enough moneyincorrect_cvc- Wrong security codeexpired_card- Card expired
Subscription errors:
resource_missing- Price or product doesn't existpayment_intent_authentication_failure- 3DS failed
Framework-Specific Guides
Next.js App Router
// app/api/create-payment-intent/route.ts
import { NextRequest } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: NextRequest) {
const { amount } = await req.json();
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
automatic_payment_methods: { enabled: true },
});
return Response.json({ clientSecret: paymentIntent.client_secret });
}
React Hook
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
export function useStripeCheckout() {
const [loading, setLoading] = useState(false);
const checkout = async (priceId: string) => {
setLoading(true);
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { sessionId } = await res.json();
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
await stripe?.redirectToCheckout({ sessionId });
setLoading(false);
};
return { checkout, loading };
}
TypeScript Types & Configuration
Database Schema Types
// types/stripe.ts
import Stripe from 'stripe';
export interface StripeCustomerData {
stripeCustomerId: string;
stripeSubscriptionId?: string;
stripePriceId?: string;
stripeCurrentPeriodEnd?: Date;
userId: string;
}
export interface CreateCheckoutSessionParams {
userId: string;
priceId: string;
successUrl: string;
cancelUrl: string;
metadata?: Record<string, string>;
}
export interface WebhookEventData {
event: Stripe.Event;
customerId?: string;
subscriptionId?: string;
invoiceId?: string;
}
export type StripeSubscriptionStatus =
| 'incomplete'
| 'incomplete_expired'
| 'trialing'
| 'active'
| 'past_due'
| 'canceled'
| 'unpaid';
export interface SubscriptionUpdate {
subscriptionId: string;
status: StripeSubscriptionStatus;
currentPeriodEnd: Date;
cancelAtPeriodEnd: boolean;
priceId: string;
}
Stripe Client Configuration
// lib/stripe/config.ts
import Stripe from 'stripe';
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY is not set');
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2024-11-20.acacia',
typescript: true,
appInfo: {
name: 'Your App Name',
version: '1.0.0',
},
// Retry logic for network failures
maxNetworkRetries: 2,
timeout: 30000, // 30 seconds
});
// Client-side configuration
export const getStripe = () => {
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
throw new Error('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set');
}
return loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
};
Environment Variables
# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Production
STRIPE_SECRET_KEY=sk_live_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Next.js App Router Integration
Server Actions for Payments
// app/actions/stripe.ts
'use server';
import { stripe } from '@/lib/stripe/config';
import { auth } from '@/lib/auth';
import { revalidatePath } from 'next/cache';
export async function createCheckoutSession(priceId: string) {
const session = await auth();
if (!session?.user?.id) {
throw new Error('Unauthorized');
}
const checkoutSession = await stripe.checkout.sessions.create({
customer_email: session.user.email,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{
price: priceId,
quantity: 1,
}],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing?canceled=true`,
metadata: {
userId: session.user.id,
},
});
return { url: checkoutSession.url };
}
export async function createPortalSession() {
const session = await auth();
if (!session?.user?.stripeCustomerId) {
throw new Error('No Stripe customer found');
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: session.user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
});
return { url: portalSession.url };
}
export async function cancelSubscription(subscriptionId: string) {
const session = await auth();
if (!session?.user?.id) {
throw new Error('Unauthorized');
}
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
revalidatePath('/dashboard');
return { success: true };
}
Client Component with Server Actions
// app/components/PricingCard.tsx
'use client';
import { useState } from 'react';
import { createCheckoutSession } from '@/app/actions/stripe';
export function PricingCard({ priceId, name, price, features }) {
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
try {
setLoading(true);
const { url } = await createCheckoutSession(priceId);
if (url) window.location.href = url;
} catch (error) {
console.error('Checkout error:', error);
alert('Failed to start checkout');
} finally {
setLoading(false);
}
};
return (
<div className="pricing-card">
<h3>{name}</h3>
<p>${price}/mo</p>
<ul>
{features.map(f => <li key={f}>{f}</li>)}
</ul>
<button disabled={loading}>
{loading ? 'Loading...' : 'Subscribe'}
</button>
</div>
);
}
Route Handlers
// app/api/create-payment-intent/route.ts
import { NextRequest } from 'next/server';
import { stripe } from '@/lib/stripe/config';
import { auth } from '@/lib/auth';
export async function POST(req: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const { amount, currency = 'usd' } = await req.json();
// Validate amount server-side (CRITICAL)
if (!amount || amount < 50) { // $0.50 minimum
return Response.json({ error: 'Invalid amount' }, { status: 400 });
}
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
automatic_payment_methods: { enabled: true },
metadata: {
userId: session.user.id,
},
});
return Response.json({
clientSecret: paymentIntent.client_secret
});
} catch (error) {
console.error('Payment intent error:', error);
return Response.json(
{ error: 'Failed to create payment intent' },
{ status: 500 }
);
}
}
Production Patterns
Error Handling
// lib/stripe/errors.ts
import Stripe from 'stripe';
export function handleStripeError(error: unknown): {
message: string;
userMessage: string;
code?: string;
} {
if (error instanceof Stripe.errors.StripeError) {
// Card errors
if (error.type === 'StripeCardError') {
return {
message: error.message,
userMessage: 'Your card was declined. Please try a different payment method.',
code: error.code,
};
}
// Rate limit errors
if (error.type === 'StripeRateLimitError') {
return {
message: 'Too many requests',
userMessage: 'Too many requests. Please try again in a moment.',
};
}
// Invalid request errors
if (error.type === 'StripeInvalidRequestError') {
return {
message: error.message,
userMessage: 'Invalid request. Please contact support.',
code: error.code,
};
}
// API errors
if (error.type === 'StripeAPIError') {
return {
message: 'Stripe API error',
userMessage: 'Payment service temporarily unavailable. Please try again.',
};
}
// Connection errors
if (error.type === 'StripeConnectionError') {
return {
message: 'Network error',
userMessage: 'Network error. Please check your connection and try again.',
};
}
// Authentication errors
if (error.type === 'StripeAuthenticationError') {
return {
message: 'Authentication failed',
userMessage: 'Authentication error. Please contact support.',
};
}
}
// Generic error
return {
message: error instanceof Error ? error.message : 'Unknown error',
userMessage: 'An unexpected error occurred. Please try again.',
};
}
// Usage in API routes
export async function POST(req: NextRequest) {
try {
const paymentIntent = await stripe.paymentIntents.create({...});
return Response.json({ clientSecret: paymentIntent.client_secret });
} catch (error) {
const { userMessage, message } = handleStripeError(error);
console.error('Stripe error:', message);
return Response.json({ error: userMessage }, { status: 400 });
}
}
Idempotency Keys
// lib/stripe/idempotency.ts
import { v4 as uuidv4 } from 'uuid';
export async function createPaymentWithIdempotency(
amount: number,
userId: string
) {
const idempotencyKey = `payment_${userId}_${Date.now()}_${uuidv4()}`;
try {
const paymentIntent = await stripe.paymentIntents.create(
{
amount,
currency: 'usd',
metadata: { userId },
},
{
idempotencyKey, // Prevents duplicate charges on retry
}
);
return paymentIntent;
} catch (error) {
// If request fails, retry with same idempotency key
// Stripe will return the original result instead of creating duplicate
throw error;
}
}
// For subscriptions
export async function createSubscriptionIdempotent(
customerId: string,
priceId: string
) {
const idempotencyKey = `sub_${customerId}_${priceId}`;
return stripe.subscriptions.create(
{
customer: customerId,
items: [{ price: priceId }],
},
{ idempotencyKey }
);
}
Retry Logic with Exponential Backoff
// lib/stripe/retry.ts
export async function retryStripeOperation<T>(
operation: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
// Don't retry certain errors
if (error instanceof Stripe.errors.StripeCardError) {
throw error; // Card declined - don't retry
}
if (error instanceof Stripe.errors.StripeInvalidRequestError) {
throw error; // Invalid params - don't retry
}
// Retry with exponential backoff
const delay = Math.min(1000 * Math.pow(2, i), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
// Usage
const paymentIntent = await retryStripeOperation(() =>
stripe.paymentIntents.create({
amount: 1000,
currency: 'usd',
})
);
Rate Limiting
// lib/stripe/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
});
export async function POST(req: NextRequest) {
const ip = req.ip ?? '127.0.0.1';
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return Response.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
);
}
// Process Stripe request
const paymentIntent = await stripe.paymentIntents.create({...});
return Response.json({ clientSecret: paymentIntent.client_secret });
}
Stripe Connect Deep Dive
Account Creation & Onboarding
// lib/stripe/connect.ts
export async function createConnectedAccount(email: string, country: string) {
const account = await stripe.accounts.create({
type: 'express', // or 'standard' or 'custom'
country,
email,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
business_type: 'individual', // or 'company'
});
return account;
}
export async function createAccountOnboardingLink(accountId: string) {
const accountLink = await stripe.accountLinks.create({
account: accountId,
refresh_url: `${process.env.NEXT_PUBLIC_URL}/connect/reauth`,
return_url: `${process.env.NEXT_PUBLIC_URL}/connect/return`,
type: 'account_onboarding',
});
return accountLink.url;
}
// Check if account is fully onboarded
export async function isAccountOnboarded(accountId: string): Promise<boolean> {
const account = await stripe.accounts.retrieve(accountId);
return (
account.details_submitted &&
account.charges_enabled &&
account.payouts_enabled
);
}
Marketplace Payment Flows
// Pattern 1: Direct Charge (platform receives, then transfers)
export async function createDirectCharge(
amount: number,
connectedAccountId: string
) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
application_fee_amount: Math.floor(amount * 0.1), // 10% platform fee
transfer_data: {
destination: connectedAccountId,
},
});
return paymentIntent;
}
// Pattern 2: Destination Charge (connected account receives, platform takes fee)
export async function createDestinationCharge(
amount: number,
connectedAccountId: string
) {
const paymentIntent = await stripe.paymentIntents.create(
{
amount,
currency: 'usd',
application_fee_amount: Math.floor(amount * 0.1),
},
{
stripeAccount: connectedAccountId, // Charge appears on connected account
}
);
return paymentIntent;
}
// Pattern 3: Separate Transfers
export async function createSeparateTransfer(
amount: number,
connectedAccountId: string,
chargeId: string
) {
const platformFee = Math.floor(amount * 0.1);
const transferAmount = amount - platformFee;
const transfer = await stripe.transfers.create({
amount: transferAmount,
currency: 'usd',
destination: connectedAccountId,
source_transaction: chargeId,
});
return transfer;
}
Multi-Party Payments (Split Payments)
// Split payment between multiple sellers
export async function createMultiPartyPayment(
totalAmount: number,
sellers: Array<{ accountId: string; amount: number }>
) {
// Create charge on platform account
const paymentIntent = await stripe.paymentIntents.create({
amount: totalAmount,
currency: 'usd',
});
// After charge succeeds, create transfers to each seller
const transfers = await Promise.all(
sellers.map(seller =>
stripe.transfers.create({
amount: seller.amount,
currency: 'usd',
destination: seller.accountId,
})
)
);
return { paymentIntent, transfers };
}
Connect Webhooks
// Listen for Connect account events
export async function POST(req: Request) {
const event = await verifyWebhook(req);
switch (event.type) {
case 'account.updated':
const account = event.data.object as Stripe.Account;
// Check if account completed onboarding
if (account.details_submitted && account.charges_enabled) {
await updateDatabase({ accountId: account.id, status: 'active' });
}
break;
case 'account.application.deauthorized':
// User disconnected their account
const deauthAccount = event.data.object as Stripe.Account;
await handleAccountDisconnection(deauthAccount.id);
break;
case 'capability.updated':
// Track capability status (card_payments, transfers, etc.)
const capability = event.data.object;
await trackCapabilityStatus(capability);
break;
case 'payout.paid':
// Payout to connected account succeeded
const payout = event.data.object as Stripe.Payout;
await notifyVendorPayoutSuccess(payout);
break;
case 'payout.failed':
// Payout failed - notify vendor
const failedPayout = event.data.object as Stripe.Payout;
await notifyVendorPayoutFailure(failedPayout);
break;
}
return Response.json({ received: true });
}
Stripe Terminal (In-Person Payments)
Reader Setup
// lib/stripe/terminal.ts
export async function registerReader(
registrationCode: string,
label: string,
locationId: string
) {
const reader = await stripe.terminal.readers.create({
registration_code: registrationCode,
label,
location: locationId,
});
return reader;
}
export async function createTerminalLocation(address: {
line1: string;
city: string;
state: string;
postal_code: string;
country: string;
}) {
const location = await stripe.terminal.locations.create({
display_name: 'Retail Store',
address,
});
return location;
}
Payment Collection
// Create payment intent for Terminal
export async function createTerminalPaymentIntent(amount: number) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
payment_method_types: ['card_present'],
capture_method: 'manual', // Authorize first, capture later
});
return paymentIntent;
}
// Server-side: Process Terminal connection token
export async function POST(req: NextRequest) {
const connectionToken = await stripe.terminal.connectionTokens.create();
return Response.json({ secret: connectionToken.secret });
}
Client-Side Terminal Integration
// Client component for Terminal SDK
'use client';
import { loadStripeTerminal } from '@stripe/terminal-js';
export function TerminalReader() {
useEffect(() => {
const initTerminal = async () => {
const terminal = await loadStripeTerminal();
const term = terminal.create({
onFetchConnectionToken: async () => {
const res = await fetch('/api/terminal/connection-token');
const { secret } = await res.json();
return secret;
},
onUnexpectedReaderDisconnect: () => {
console.log('Reader disconnected');
},
});
// Discover readers
const discoverResult = await term.discoverReaders();
if (discoverResult.discoveredReaders.length > 0) {
// Connect to first reader
await term.connectReader(discoverResult.discoveredReaders[0]);
}
};
initTerminal();
}, []);
return <div>Terminal Reader Component</div>;
}
Stripe Radar (Fraud Prevention)
Custom Fraud Rules
// Review high-risk charges before processing
export async function reviewHighRiskCharge(paymentIntentId: string) {
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId, {
expand: ['latest_charge'],
});
const charge = paymentIntent.latest_charge as Stripe.Charge;
const outcome = charge?.outcome;
if (outcome?.risk_level === 'highest') {
// Block the payment
await stripe.paymentIntents.cancel(paymentIntentId);
return { action: 'blocked', reason: outcome.reason };
}
if (outcome?.risk_level === 'elevated' && outcome?.risk_score > 65) {
// Require manual review
return { action: 'review', riskScore: outcome.risk_score };
}
return { action: 'approve' };
}
// Add custom fraud metadata
export async function createPaymentWithFraudCheck(
amount: number,
email: string,
ipAddress: string
) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
metadata: {
customer_email: email,
customer_ip: ipAddress,
order_id: generateOrderId(),
},
// Radar uses this data for fraud detection
});
return paymentIntent;
}
3D Secure Authentication
// Force 3D Secure for high-value transactions
export async function createSecurePayment(amount: number) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
payment_method_options: {
card: {
request_three_d_secure: amount > 10000 ? 'any' : 'automatic',
},
},
});
return paymentIntent;
}
// Handle 3DS authentication in client
async function handleCardAction(clientSecret: string) {
const stripe = await getStripe();
const { error, paymentIntent } = await stripe.handleCardAction(clientSecret);
if (error) {
console.error('3DS failed:', error);
return { success: false };
}
if (paymentIntent.status === 'requires_confirmation') {
// Confirm on server
await fetch('/api/confirm-payment', {
method: 'POST',
body: JSON.stringify({ paymentIntentId: paymentIntent.id }),
});
}
return { success: true };
}
Reviewing Disputes
// Handle dispute webhooks
case 'charge.dispute.created':
const dispute = event.data.object as Stripe.Dispute;
// Automatically submit evidence for low-amount disputes
if (dispute.amount < 5000) { // $50
await stripe.disputes.update(dispute.id, {
evidence: {
customer_email_address: dispute.metadata.customer_email,
customer_name: dispute.metadata.customer_name,
shipping_tracking_number: dispute.metadata.tracking_number,
},
});
} else {
// Flag for manual review
await notifyDisputeTeam(dispute);
}
break;
case 'charge.dispute.closed':
const closedDispute = event.data.object as Stripe.Dispute;
if (closedDispute.status === 'won') {
await notifyDisputeWon(closedDispute);
} else {
await processDisputeLoss(closedDispute);
}
break;
Stripe Identity
Identity Verification
// Create verification session
export async function createIdentityVerification(email: string) {
const verificationSession = await stripe.identity.verificationSessions.create({
type: 'document',
metadata: {
user_email: email,
},
options: {
document: {
require_live_capture: true,
require_matching_selfie: true,
allowed_types: ['driving_license', 'passport', 'id_card'],
},
},
});
return {
clientSecret: verificationSession.client_secret,
url: verificationSession.url,
};
}
// Check verification status
export async function checkVerificationStatus(sessionId: string) {
const session = await stripe.identity.verificationSessions.retrieve(sessionId);
return {
status: session.status, // 'requires_input', 'verified', 'canceled'
verified: session.status === 'verified',
lastError: session.last_error,
};
}
// Webhook handler
case 'identity.verification_session.verified':
const verifiedSession = event.data.object;
await updateUserVerificationStatus(
verifiedSession.metadata.user_email,
'verified'
);
break;
case 'identity.verification_session.requires_input':
const requiresInputSession = event.data.object;
await notifyUserVerificationIssue(requiresInputSession);
break;
Stripe Issuing (Card Creation)
Create Virtual/Physical Cards
// Create cardholder
export async function createCardholder(
name: string,
email: string,
phone: string
) {
const cardholder = await stripe.issuing.cardholders.create({
name,
email,
phone_number: phone,
billing: {
address: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94111',
country: 'US',
},
},
type: 'individual',
});
return cardholder;
}
// Create virtual card
export async function createVirtualCard(cardholderId: string) {
const card = await stripe.issuing.cards.create({
cardholder: cardholderId,
currency: 'usd',
type: 'virtual',
status: 'active',
spending_controls: {
spending_limits: [{
amount: 100000, // $1,000 limit
interval: 'monthly',
}],
allowed_categories: ['food_restaurants', 'gas_stations'],
},
});
return card;
}
// Retrieve card details (PAN, CVV, etc.) - SENSITIVE
export async function getCardDetails(cardId: string) {
const card = await stripe.issuing.cards.retrieve(cardId, {
expand: ['number', 'cvc'],
});
return {
number: card.number,
cvc: card.cvc,
expMonth: card.exp_month,
expYear: card.exp_year,
};
}
Authorization Controls
// Webhook: Real-time authorization approval
case 'issuing_authorization.request':
const authorization = event.data.object as Stripe.Issuing.Authorization;
// Check spending limits
const userLimits = await getUserSpendingLimits(authorization.cardholder);
if (authorization.amount > userLimits.perTransaction) {
// Decline authorization
await stripe.issuing.authorizations.decline(authorization.id, {
metadata: { reason: 'exceeds_limit' },
});
} else {
// Approve authorization
await stripe.issuing.authorizations.approve(authorization.id);
}
break;
case 'issuing_authorization.created':
// Log transaction for user
await logCardTransaction(event.data.object);
break;
Stripe Tax
Automatic Tax Calculation
// Enable tax on Checkout
export async function createCheckoutWithTax(priceId: string) {
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
automatic_tax: { enabled: true },
customer_update: {
address: 'auto', // Collect address for tax calculation
shipping: 'auto',
},
success_url: `${process.env.NEXT_PUBLIC_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cancel`,
});
return session;
}
// Calculate tax for custom amount
export async function calculateTax(
amount: number,
customerAddress: {
line1: string;
city: string;
state: string;
postal_code: string;
country: string;
}
) {
const calculation = await stripe.tax.calculations.create({
currency: 'usd',
line_items: [{
amount,
reference: 'product_001',
}],
customer_details: {
address: customerAddress,
address_source: 'shipping',
},
});
return {
amountTotal: calculation.amount_total,
taxAmount: calculation.tax_amount_exclusive,
taxBreakdown: calculation.tax_breakdown,
};
}
// Tax-inclusive pricing
export async function createTaxInclusivePrice() {
const price = await stripe.prices.create({
unit_amount: 1000,
currency: 'usd',
product: 'prod_xxx',
tax_behavior: 'inclusive', // Tax is included in price
});
return price;
}
Stripe Treasury (Banking-as-a-Service)
Financial Accounts
// Create financial account for user
export async function createFinancialAccount(userId: string) {
const financialAccount = await stripe.treasury.financialAccounts.create({
supported_currencies: ['usd'],
features: {
card_issuing: { requested: true },
deposit_insurance: { requested: true },
financial_addresses: { aba: { requested: true } },
inbound_transfers: { ach: { requested: true } },
outbound_payments: {
ach: { requested: true },
us_domestic_wire: { requested: true },
},
…(truncated)