| name | stripe |
| description | Stripe Checkout, subscriptions, webhooks, customer portal, payment intents, test mode, idempotency |
Stripe Integration Skill
When to activate
- Implementing Stripe Checkout or Payment Intents for one-time payments
- Setting up Stripe subscriptions and billing cycles
- Handling Stripe webhooks for payment events
- Implementing refunds, disputes, or payment cancellations
- Setting up Stripe Connect for marketplace/platform payments
- Debugging failed payments, declined cards, or webhook delivery issues
- Implementing SCA (Strong Customer Authentication) compliance
When NOT to use
- PayPal, Braintree, Adyen, Mollie — different payment providers with different SDKs
- Crypto payments
- Internal accounting or invoicing systems without a payment gateway
- Bank transfer / ACH-only flows (different Stripe product — Payment Elements still apply, but flow differs)
Instructions
Never hardcode or log payment data
console.log('Payment intent:', paymentIntent);
console.log('Payment intent created:', paymentIntent.id, paymentIntent.status);
Payment Intents — server-side creation
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
});
export async function createPaymentIntent(
amount: number,
currency: string,
customerId: string,
metadata: Record<string, string>
): Promise<{ clientSecret: string; paymentIntentId: string }> {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
customer: customerId,
automatic_payment_methods: { enabled: true },
metadata,
idempotency_key: `pi_${customerId}_${Date.now()}`,
});
return {
clientSecret: paymentIntent.client_secret!,
paymentIntentId: paymentIntent.id,
};
}
Webhook handling — always verify signature
import { headers } from 'next/headers';
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response('Invalid signature', { status: 400 });
}
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
break;
case 'payment_intent.payment_failed':
(event.. .);
;
:
(event.. .);
;
:
.();
}
(, { : });
}
() {
orderId = paymentIntent..;
existing = db..({ : { : orderId } });
(existing?. === ) ;
db..({
: { : orderId },
: { : , : paymentIntent. }
});
}
Subscriptions
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
Test mode discipline
const isLiveMode = process.env.STRIPE_SECRET_KEY!.startsWith('sk_live_');
if (isLiveMode && process.env.NODE_ENV !== 'production') {
throw new Error('Live Stripe keys must not be used outside production');
}
Error handling
try {
const charge = await stripe.charges.create({ ... });
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
return { error: err.message, code: err.code };
}
if (err instanceof Stripe.errors.StripeRateLimitError) {
throw err;
}
if (err instanceof Stripe.errors.StripeInvalidRequestError) {
console.error('Invalid Stripe request:', err.message);
throw err;
}
throw err;
}
Example
User: Implement a checkout flow for a SaaS product: create a payment intent server-side, handle the webhook on success to activate the subscription, and handle failed payments.
Expected output:
POST /api/checkout — creates PaymentIntent, returns clientSecret
POST /api/webhooks/stripe — verifies signature, handles payment_intent.succeeded (idempotent DB update), payment_intent.payment_failed (log + notify)
- Metadata on PaymentIntent:
user_id, plan_id, order_id
- All amounts in cents, currency explicit
- API version pinned
- No logging of sensitive data