From Claudient — Finance & Payments
Stripe Checkout, subscriptions, webhooks, customer portal, payment intents, test mode, idempotency
How this skill is triggered — by the user, by Claude, or both
Slash command
/claudient-finance-payments:stripeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Implementing Stripe Checkout or Payment Intents for one-time payments
// NEVER log card details, full payment intent objects, or customer PII
// BAD:
console.log('Payment intent:', paymentIntent); // May contain sensitive data
// GOOD:
console.log('Payment intent created:', paymentIntent.id, paymentIntent.status);
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia', // Always pin the API version
});
// Create payment intent server-side — never client-side
export async function createPaymentIntent(
amount: number, // Always in smallest currency unit (cents for USD)
currency: string,
customerId: string,
metadata: Record<string, string>
): Promise<{ clientSecret: string; paymentIntentId: string }> {
const paymentIntent = await stripe.paymentIntents.create({
amount, // e.g., 2999 = $29.99
currency, // e.g., 'usd', 'eur'
customer: customerId,
automatic_payment_methods: { enabled: true },
metadata, // Store your internal IDs here
idempotency_key: `pi_${customerId}_${Date.now()}`, // Prevent duplicate charges
});
return {
clientSecret: paymentIntent.client_secret!,
paymentIntentId: paymentIntent.id,
};
}
import { headers } from 'next/headers';
export async function POST(request: Request) {
const body = await request.text(); // Raw body — not parsed JSON
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
// Invalid signature — reject immediately
return new Response('Invalid signature', { status: 400 });
}
// Process idempotently — webhooks can be delivered more than once
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailed(event.data.object as Stripe.PaymentIntent);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(event.data.object as Stripe.Subscription);
break;
default:
// Log but don't error on unknown events — Stripe adds new events over time
console.log(`Unhandled event type: ${event.type}`);
}
return new Response(null, { status: 200 });
}
// Always check if already processed before making DB changes
async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
const orderId = paymentIntent.metadata.order_id;
// Idempotency check
const existing = await db.order.findUnique({ where: { id: orderId } });
if (existing?.status === 'paid') return; // Already processed
await db.order.update({
where: { id: orderId },
data: { status: 'paid', stripePaymentIntentId: paymentIntent.id }
});
}
// Create subscription
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete', // Don't activate until payment confirmed
expand: ['latest_invoice.payment_intent'],
});
// Key webhook events for subscriptions:
// customer.subscription.created → provision access
// customer.subscription.updated → handle plan changes
// customer.subscription.deleted → revoke access
// invoice.payment_succeeded → extend access period
// invoice.payment_failed → send dunning email, then suspend after grace period
// Use test card numbers in development:
// Success: 4242 4242 4242 4242
// Decline: 4000 0000 0000 0002
// Auth required: 4000 0025 0000 3155
// Use test webhook CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe
// Never use test keys in production, never use live keys in development
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');
}
try {
const charge = await stripe.charges.create({ ... });
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// Card declined — show user-friendly message
return { error: err.message, code: err.code };
}
if (err instanceof Stripe.errors.StripeRateLimitError) {
// Retry with exponential backoff
throw err;
}
if (err instanceof Stripe.errors.StripeInvalidRequestError) {
// Bad API call — log and fix the code
console.error('Invalid Stripe request:', err.message);
throw err;
}
// Other errors: StripeAPIError, StripeConnectionError, StripeAuthenticationError
throw err;
}
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 clientSecretPOST /api/webhooks/stripe — verifies signature, handles payment_intent.succeeded (idempotent DB update), payment_intent.payment_failed (log + notify)user_id, plan_id, order_idWork with us: Claudient is backed by Uitbreiden — we build AI products and B2B solutions with developer communities. Building payment flows or monetizing AI products? uitbreiden.com · Reddit · YouTube
Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub claudient/claudient --plugin claudient-finance-payments