Payment processing stands as one of the most critical and complex components of modern applications. Whether you're building a SaaS platform, marketplace, or subscription service, implementation quality directly impacts revenue and customer trust. Stripe's API is powerful but intricate, with numerous security considerations and edge cases.
Antigravity's AI agent dramatically accelerates Stripe integration by automating API documentation lookup, generating secure payment flows, creating webhook handlers, and enforcing security best practices. This guide demonstrates how to build production-grade payment systems with AI assistance.
Understanding Stripe Architecture
Payment Flow Overview
Stripe payment processing involves these key entities:
Customer selects product
↓
Create Checkout Session
↓
Redirect to Stripe Checkout
↓
Customer enters payment details
↓
Stripe processes (tokenizes) card securely
↓
Payment Intent succeeds
↓
Webhook: payment_intent.succeeded
↓
Your server confirms purchase, sends email, grants access
Core Stripe Concepts
| Entity | Purpose |
|---|---|
| Customer | Represents a customer in your system |
| Payment Method | Card, bank account, or digital wallet |
| Checkout Session | Shopping cart converted to payment request |
| Payment Intent | Low-level API for payment processing |
| Subscription | Recurring billing with automatic invoicing |
| Invoice | Billing document with payment history |
| Webhook | Event notifications for asynchronous updates |
Setting Up Stripe + Antigravity
Step 1: Configure Environment
# .env.local
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxStep 2: Integration Configuration
# .antigravity/integrations/stripe.yml
stripe:
enabled: true
environment: "test" # test or production
api_keys:
publishable: "${STRIPE_PUBLISHABLE_KEY}"
secret: "${STRIPE_SECRET_KEY}"
webhook: "${STRIPE_WEBHOOK_SECRET}"
reference_docs:
api: "https://stripe.com/docs/api"
libraries:
nodejs: "https://github.com/stripe/stripe-node"
python: "https://github.com/stripe/stripe-python"
test_cards:
success: "4242 4242 4242 4242"
declined: "4000 0000 0000 0002"
requires_auth: "4000 0025 0000 3155"
security:
verify_webhook_signatures: true
require_https: true
pci_compliance_mode: trueStep 3: Initialize Stripe Agent
antigravity agents create stripe-payment-agent \
--template stripe-integration \
--language typescript \
--framework nextjsGenerated agent structure:
agents/stripe-payment-agent/
├── skills.sh
├── checkout.ts
├── subscriptions.ts
├── webhooks.ts
├── customers.ts
└── testing.ts
Implementation Patterns
Pattern 1: One-Time Payments
Simple, one-off transactions using Checkout Sessions:
// app/api/checkout/route.ts
import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
const { productId, quantity, userId, email } = await request.json();
// Fetch product details
const product = await getProductById(productId);
if (!product) {
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
}
try {
// Create Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: product.name,
images: [product.imageUrl],
description: product.description,
metadata: {
productId: productId
}
},
unit_amount: Math.round(product.price * 100) // Convert to cents
},
quantity: quantity
}
],
metadata: {
userId: userId,
productId: productId
},
customer_email: email,
success_url: `${process.env.NEXT_PUBLIC_DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_DOMAIN}/cancelled`,
mode: 'payment'
});
return NextResponse.json({ sessionId: session.id });
} catch (error) {
console.error('Checkout session creation failed:', error);
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
);
}
}Antigravity Usage:
Developer: "Generate a secure checkout endpoint using Stripe Sessions"
Antigravity AI:
1. References Stripe API documentation for checkout sessions
2. Generates TypeScript code with proper error handling
3. Adds Stripe library type safety
4. Implements proper environment variable handling
5. Generates unit tests
6. Adds security validation
Pattern 2: Subscription Billing
Recurring revenue requires careful handling of customer lifecycle:
// app/api/subscriptions/create/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
const { userId, priceId, paymentMethodId, email } = await request.json();
try {
// Get or create customer
let customer: Stripe.Customer;
const existingCustomers = await stripe.customers.list({
email: email,
limit: 1
});
if (existingCustomers.data.length > 0) {
customer = existingCustomers.data[0];
} else {
customer = await stripe.customers.create({
email: email,
metadata: { userId: userId }
});
}
// Attach payment method to customer
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customer.id
});
// Set as default payment method
await stripe.customers.update(customer.id, {
invoice_settings: {
default_payment_method: paymentMethodId
}
});
// Create subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
payment_settings: {
save_default_payment_method: 'on_subscription'
},
metadata: {
userId: userId
},
// Optional: Trial period
trial_period_days: 14,
// Automatic renewal
cancel_at_period_end: false
});
// Save to database
await db.subscription.create({
userId,
stripeSubscriptionId: subscription.id,
stripeCustomerId: customer.id,
status: subscription.status
});
return NextResponse.json({
subscriptionId: subscription.id,
status: subscription.status
});
} catch (error) {
console.error('Subscription creation failed:', error);
return NextResponse.json(
{ error: 'Failed to create subscription' },
{ status: 500 }
);
}
}Subscription Cancellation:
// app/api/subscriptions/cancel/route.ts
export async function POST(request: NextRequest) {
const { userId, subscriptionId } = await request.json();
// Verify ownership
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const dbSubscription = await db.subscription.findUnique({
where: { stripeSubscriptionId: subscriptionId }
});
if (dbSubscription?.userId !== userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
}
try {
// Cancel at period end (graceful cancellation)
const cancelled = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true
});
// Update database
await db.subscription.update({
where: { stripeSubscriptionId: subscriptionId },
data: {
status: 'cancel_at_period_end',
cancelledAt: new Date()
}
});
// Send notification email
await sendSubscriptionCancellationEmail(dbSubscription.user.email);
return NextResponse.json({ status: 'cancelled' });
} catch (error) {
return NextResponse.json(
{ error: 'Cancellation failed' },
{ status: 500 }
);
}
}Pattern 3: Webhook Event Handling
Webhooks synchronize Stripe with your application. This is critical and complex—exactly where Antigravity excels:
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
// Event type definitions
type StripeEventType =
| 'payment_intent.succeeded'
| 'payment_intent.payment_failed'
| 'charge.refunded'
| 'customer.subscription.created'
| 'customer.subscription.updated'
| 'customer.subscription.deleted'
| 'invoice.payment_succeeded'
| 'invoice.payment_failed';
// Handler registry
const eventHandlers: Record<
StripeEventType,
(event: Stripe.Event) => Promise<void>
> = {
'payment_intent.succeeded': handlePaymentIntentSucceeded,
'payment_intent.payment_failed': handlePaymentIntentFailed,
'charge.refunded': handleChargeRefunded,
'customer.subscription.created': handleSubscriptionCreated,
'customer.subscription.updated': handleSubscriptionUpdated,
'customer.subscription.deleted': handleSubscriptionDeleted,
'invoice.payment_succeeded': handleInvoicePaymentSucceeded,
'invoice.payment_failed': handleInvoicePaymentFailed
};
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('stripe-signature');
// Verify Stripe signature
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature!, endpointSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// Route to handler
const handler = eventHandlers[event.type as StripeEventType];
if (!handler) {
console.warn(`Unhandled event type: ${event.type}`);
return NextResponse.json({ received: true });
}
try {
await handler(event);
} catch (error) {
console.error(`Error processing ${event.type}:`, error);
// Store in dead letter queue for retry
await db.webhookLog.create({
eventId: event.id,
status: 'failed',
error: String(error)
});
}
return NextResponse.json({ received: true });
}
// Handler implementations
async function handlePaymentIntentSucceeded(event: Stripe.Event) {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const userId = paymentIntent.metadata?.userId;
// Record purchase
await db.purchase.create({
userId,
stripePaymentIntentId: paymentIntent.id,
amount: paymentIntent.amount,
status: 'completed'
});
// Grant access
if (paymentIntent.metadata?.productId) {
await grantProductAccess(userId, paymentIntent.metadata.productId);
}
// Send confirmation
const user = await db.user.findUnique({ where: { id: userId } });
await sendPurchaseConfirmationEmail(user!.email, paymentIntent);
}
async function handlePaymentIntentFailed(event: Stripe.Event) {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const userId = paymentIntent.metadata?.userId;
// Log failure
await db.paymentFailure.create({
userId,
stripePaymentIntentId: paymentIntent.id,
reason: paymentIntent.last_payment_error?.message
});
// Notify user
const user = await db.user.findUnique({ where: { id: userId } });
await sendPaymentFailedEmail(
user!.email,
paymentIntent.last_payment_error?.message
);
}
async function handleSubscriptionCreated(event: Stripe.Event) {
const subscription = event.data.object as Stripe.Subscription;
const userId = subscription.metadata?.userId;
// Save subscription
await db.subscription.create({
userId,
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
status: subscription.status
});
// Grant plan access
const priceId = subscription.items.data[0].price.id;
const plan = await db.plan.findUnique({ where: { stripePriceId: priceId } });
await grantPlanAccess(userId, plan!.name);
}
async function handleSubscriptionDeleted(event: Stripe.Event) {
const subscription = event.data.object as Stripe.Subscription;
const userId = subscription.metadata?.userId;
// Update database
await db.subscription.update({
where: { stripeSubscriptionId: subscription.id },
data: { status: 'cancelled', cancelledAt: new Date() }
});
// Revoke access
await revokePlanAccess(userId);
// Notify user
const user = await db.user.findUnique({ where: { id: userId } });
await sendSubscriptionCancelledEmail(user!.email);
}
async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
const invoice = event.data.object as Stripe.Invoice;
// Update subscription status
if (invoice.subscription) {
await db.subscription.update({
where: { stripeSubscriptionId: invoice.subscription as string },
data: { lastBilledAt: new Date(invoice.created * 1000) }
});
}
}
async function handleInvoicePaymentFailed(event: Stripe.Event) {
const invoice = event.data.object as Stripe.Invoice;
// Alert about failed renewal
if (invoice.customer_email) {
await sendRenewalFailedEmail(invoice.customer_email, invoice);
}
}
async function handleChargeRefunded(event: Stripe.Event) {
const charge = event.data.object as Stripe.Charge;
// Log refund
await db.refund.create({
stripeChargeId: charge.id,
amount: charge.amount_refunded,
reason: charge.refund_reason
});
// Notify user
const customer = await stripe.customers.retrieve(charge.customer as string);
await sendRefundConfirmationEmail(customer.email!, charge);
}Pattern 4: Customer Portal
Allow customers to manage subscriptions without coding:
// app/api/customer-portal/route.ts
export async function POST(request: NextRequest) {
const { userId } = await request.json();
const user = await db.user.findUnique({ where: { id: userId } });
const subscription = await db.subscription.findFirst({
where: { userId }
});
if (!subscription) {
return NextResponse.json(
{ error: 'No subscription found' },
{ status: 404 }
);
}
try {
const portalSession = await stripe.billingPortal.sessions.create({
customer: subscription.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_DOMAIN}/account/billing`
});
return NextResponse.json({ url: portalSession.url });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create portal session' },
{ status: 500 }
);
}
}Local Development with Stripe CLI
Test webhooks locally without deploying:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Authenticate
stripe login
# Forward webhooks to localhost
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
stripe trigger invoice.payment_succeededAntigravity Integration:
Developer: "Generate comprehensive webhook tests using Stripe CLI"
Antigravity generates:
1. CLI command reference
2. Mock event payloads
3. Jest test suite covering all events
4. Error scenario tests
5. CI/CD integration
Security Best Practices
Absolute Don'ts
// ❌ NEVER handle card data server-side
async function processPayment(cardNumber, expiry, cvc) {
// This violates PCI compliance
const charge = await stripe.charges.create({
amount: 5000,
currency: 'usd',
card: {
number: cardNumber,
exp_month: expiry.month,
exp_year: expiry.year,
cvc: cvc
}
});
}
// ✅ Use Stripe.js on client
// Client-side code
const { token } = await stripe.createToken(cardElement);
// Server-side: handle only the token
async function processPayment(token) {
const charge = await stripe.charges.create({
amount: 5000,
currency: 'usd',
source: token // Safe: PCI-compliant token
});
}Security Checklist
✅ API Keys:
- Store in environment variables
- Never commit to version control
- Rotate periodically
- Use test keys for development
✅ Payment Methods:
- Never log full card numbers
- Use tokenization (Stripe.js)
- Implement 3D Secure for card authentication
- Support multiple payment methods
✅ Webhooks:
- Always verify signatures
- Handle idempotently (allow retries)
- Use HTTPS only
- Implement exponential backoff
✅ Customer Data:
- Encrypt sensitive fields
- Implement access controls
- Log audit trails
- GDPR/CCPA compliance
✅ Errors:
- Never expose API details to users
- Log full errors server-side
- Return generic messages to client
- Monitor error ratesCommon Issues & Solutions
Error: "No such customer"
Cause: Customer doesn't exist in Stripe
Fix: Create customer before creating subscription
Error: "Webhook signature verification failed"
Cause: Wrong STRIPE_WEBHOOK_SECRET
Fix: Copy correct value from Stripe Dashboard
Error: "Your card was declined"
Cause: Invalid test card or Stripe validation
Fix: Use test card 4242 4242 4242 4242
Error: "Idempotency key already used"
Cause: Duplicate request processing
Fix: Implement idempotency key handling
Testing Strategy
// __tests__/stripe-integration.test.ts
describe('Stripe Integration', () => {
describe('Checkout Session', () => {
it('creates session with correct amount and currency', async () => {
const response = await POST(createMockRequest({
productId: 'prod_123',
quantity: 2,
email: 'test@example.com'
}));
expect(response.status).toBe(200);
const { sessionId } = await response.json();
const session = await stripe.checkout.sessions.retrieve(sessionId);
expect(session.amount_total).toBe(10000); // 2 × $50
});
});
describe('Webhook Handling', () => {
it('processes payment.intent.succeeded correctly', async () => {
const event = createMockEvent('payment_intent.succeeded', {
id: 'pi_test_123',
amount: 5000,
metadata: { userId: 'user_123' }
});
const signature = createSignature(event);
const response = await POST(
createMockRequest(event, signature)
);
expect(response.status).toBe(200);
// Verify purchase was recorded
const purchase = await db.purchase.findUnique({
where: { stripePaymentIntentId: 'pi_test_123' }
});
expect(purchase?.status).toBe('completed');
});
});
});Wrapping up
Stripe + Antigravity dramatically accelerates payment system development:
- 60-70% faster implementation — AI generates secure code patterns
- Improved security — Best practices automatically enforced
- Comprehensive testing — Test suites auto-generated
- Scalable architecture — Handles growth from startup to enterprise
Begin with one-time payments, then progress to subscriptions. Let Antigravity guide you through Stripe's complexity while maintaining security and reliability.
Build payment systems with confidence. Your revenue depends on it.