This skill ensures Stripe payment integration is implemented correctly the first time, following best practices for security, reliability, and user experience.
When to Use This Skill
Use this skill when:
- Adding Stripe to a new application
- Implementing subscription billing
- Setting up payment processing
- Configuring webhooks
- Managing customer lifecycle
- Handling refunds or disputes
- Implementing checkout flows
Core Principles
Security First:
- Never expose secret keys client-side
- Always use HTTPS in production
- Validate webhook signatures
- Use Stripe's client-side libraries for card handling
Idempotency:
- Use idempotency keys for API requests
- Handle duplicate webhook events
- Design for retry-safety
User Experience:
- Clear error messages
- Loading states during payment
- Success/failure feedback
- Email confirmations
Implementation Workflow
1. Environment Setup
Required Credentials:
STRIPE_PUBLISHABLE_KEY_TEST=pk_test_...
STRIPE_SECRET_KEY_TEST=sk_test_...
STRIPE_WEBHOOK_SECRET_TEST=whsec_...
STRIPE_PUBLISHABLE_KEY_PROD=pk_live_...
STRIPE_SECRET_KEY_PROD=sk_live_...
STRIPE_WEBHOOK_SECRET_PROD=whsec_...
Never Commit:
- Add to .env or secrets manager
- Use .gitignore for env files
- Different keys for dev/staging/prod
Installation:
# Node.js
npm install stripe @stripe/stripe-js
# Python
pip install stripe
2. Basic Payment Flow
Client-Side (React example):
import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) return;
// Create payment intent on backend first
const { clientSecret } = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 5000 }) // $50.00
}).then(r => r.json());
// Confirm payment on client
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: 'Customer Name',
email: 'customer@example.com'
}
}
}
);
if (error) {
console.error(error.message);
} else if (paymentIntent.status === 'succeeded') {
// Payment successful
console.log('Payment succeeded!');
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={!stripe}>
Pay
</button>
</form>
);
}
Server-Side (Node.js example):
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/api/create-payment-intent', async (req, res) => {
try {
const { amount } = req.body;
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd',
automatic_payment_methods: {
enabled: true,
},
metadata: {
userId: req.user.id, // Track in your DB
orderId: req.body.orderId
}
});
res.json({ clientSecret: paymentIntent.client_secret });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
3. Subscription Implementation
Create Subscription:
// Server-side
async function createSubscription(customerId, priceId) {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription'
},
expand: ['latest_invoice.payment_intent']
});
return {
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret
};
}
Handle Subscription Lifecycle:
- Customer signs up → create customer + subscription
- Payment succeeds → activate features
- Payment fails → notify customer, retry
- Cancellation requested → process at period_end
- Renewal → webhook confirms payment
4. Webhook Configuration
CRITICAL: Webhooks are how Stripe notifies you of events. Must be implemented correctly.
Endpoint Setup:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.log(`Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
await handlePaymentSuccess(paymentIntent);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
await handlePaymentFailure(failedPayment);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
await handleSubscriptionCreated(subscription);
break;
case 'customer.subscription.updated':
const updatedSub = event.data.object;
await handleSubscriptionUpdated(updatedSub);
break;
case 'customer.subscription.deleted':
const deletedSub = event.data.object;
await handleSubscriptionCanceled(deletedSub);
break;
case 'invoice.payment_succeeded':
const invoice = event.data.object;
await handleInvoicePaymentSucceeded(invoice);
break;
case 'invoice.payment_failed':
const failedInvoice = event.data.object;
await handleInvoicePaymentFailed(failedInvoice);
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
res.json({received: true});
});
Essential Events to Handle:
payment_intent.succeeded- One-time payment successfulpayment_intent.payment_failed- Payment failedcustomer.subscription.created- New subscriptioncustomer.subscription.updated- Subscription changedcustomer.subscription.deleted- Subscription canceledinvoice.payment_succeeded- Recurring payment successfulinvoice.payment_failed- Recurring payment failed
5. Customer Management
Create Customer:
const customer = await stripe.customers.create({
email: user.email,
name: user.name,
metadata: {
userId: user.id
}
});
// Store customer.id in your database
await db.users.update(user.id, {
stripeCustomerId: customer.id
});
Attach Payment Method:
const paymentMethod = await stripe.paymentMethods.attach(
pm_id,
{ customer: customerId }
);
await stripe.customers.update(customerId, {
invoice_settings: {
default_payment_method: pm_id
}
});
6. Testing
Test Mode:
- Use test keys (pk_test_, sk_test_)
- Test card numbers:
- Success:
4242 4242 4242 4242 - Declined:
4000 0000 0000 0002 - 3D Secure:
4000 0027 6000 3184
- Success:
- Use stripe CLI for webhook testing:
stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded
Webhook Testing:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:3000/webhook
# Trigger events manually
stripe trigger customer.subscription.created
7. Error Handling
Common Errors to Handle:
try {
const charge = await stripe.charges.create({...});
} catch (error) {
switch (error.type) {
case 'StripeCardError':
// Card was declined
console.log('Card declined:', error.message);
break;
case 'StripeInvalidRequestError':
// Invalid parameters
console.log('Invalid request:', error.message);
break;
case 'StripeAPIError':
// Stripe API error
console.log('API error:', error.message);
break;
case 'StripeConnectionError':
// Network error
console.log('Network error');
break;
case 'StripeAuthenticationError':
// Authentication error
console.log('Auth error');
break;
default:
console.log('Unknown error:', error);
}
}
Checklist for Production
Before going live:
- Using live API keys (pk_live_, sk_live_)
- HTTPS enabled everywhere
- Webhook endpoint configured in Stripe Dashboard
- Webhook signature verification implemented
- All critical events handled
- Idempotency keys on API calls
- Customer creation linked to user accounts
- Error handling for all payment flows
- Email notifications setup
- Refund process documented
- Tested with real test cards
- Webhook events tested via CLI
- Subscription renewal flow tested
- Cancellation flow tested
- Failed payment retry logic
- Security review completed
Common Patterns
Subscription Tiers
const plans = {
basic: 'price_basic123',
pro: 'price_pro456',
enterprise: 'price_enterprise789'
};
function subscribeToPlan(customerId, tier) {
return stripe.subscriptions.create({
customer: customerId,
items: [{ price: plans[tier] }]
});
}
Usage-Based Billing
// Record usage
await stripe.subscriptionItems.createUsageRecord(
subscriptionItemId,
{
quantity: 100,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment'
}
);
Prorated Upgrades
await stripe.subscriptions.update(subscriptionId, {
items: [{
id: subscriptionItemId,
price: newPriceId
}],
proration_behavior: 'create_prorations'
});
Resources
Official Docs:
- https://stripe.com/docs
- https://stripe.com/docs/webhooks
- https://stripe.com/docs/billing/subscriptions/overview
Testing:
Security:
Tips
- Start Simple: Get basic payments working before subscriptions
- Use Stripe Checkout: Hosted page handles complex flows
- Test Webhooks Thoroughly: Use CLI to simulate all events
- Log Everything: Track payment intents, events in your DB
- Handle Failures Gracefully: Retry logic, user notifications
- Keep Test/Prod Separate: Never mix keys or environments
- Monitor Stripe Dashboard: Check for errors and disputes
- Read Stripe Docs: They're excellent and comprehensive