Skills stripe-connect
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/stripe-connect" ~/.claude/skills/terminalskills-skills-stripe-connect && rm -rf "$T"
manifest:
skills/stripe-connect/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
Stripe Connect
Overview
Stripe Connect enables platforms to route payments between buyers, sellers, and your platform. It handles regulatory compliance (KYC), payouts, and tax reporting for connected accounts.
Account types:
| Type | Onboarding | Branding | Best for |
|---|---|---|---|
| Express | Stripe-hosted | Stripe UI | Marketplaces (recommended) |
| Standard | OAuth redirect | Seller's branding | Platforms where sellers have existing Stripe accounts |
| Custom | Fully custom | Your UI | Large platforms needing full control |
Charge types:
| Type | Who pays Stripe fees | Use when |
|---|---|---|
| Direct | Connected account | Seller wants full control |
| Destination | Platform | Platform manages UX |
| Separate charges + transfers | Platform | Complex routing |
Setup
npm install stripe
// lib/stripe.ts import Stripe from "stripe"; export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-06-20", }); // Platform account key (your Stripe account) // Connected accounts are identified by their account ID
Express Accounts (Recommended)
1. Create a connected account
// POST /api/sellers/onboard import { stripe } from "@/lib/stripe"; export async function createExpressAccount(email: string) { const account = await stripe.accounts.create({ type: "express", email, capabilities: { card_payments: { requested: true }, transfers: { requested: true }, }, business_type: "individual", }); return account.id; // Store this as seller.stripeAccountId in your DB }
2. Generate onboarding link
export async function createOnboardingLink(accountId: string, userId: string) { const accountLink = await stripe.accountLinks.create({ account: accountId, refresh_url: `${process.env.BASE_URL}/sellers/onboard/refresh?userId=${userId}`, return_url: `${process.env.BASE_URL}/sellers/onboard/complete?userId=${userId}`, type: "account_onboarding", }); return accountLink.url; // Redirect seller here } // Full flow: app.post("/api/sellers/onboard", async (req, res) => { const { email, userId } = req.body; const accountId = await createExpressAccount(email); // Save accountId to your DB await db.sellers.update(userId, { stripeAccountId: accountId }); const url = await createOnboardingLink(accountId, userId); res.json({ url }); });
3. Check onboarding status
export async function isSellerOnboarded(accountId: string): Promise<boolean> { const account = await stripe.accounts.retrieve(accountId); return account.details_submitted && !account.requirements?.currently_due?.length; } app.get("/sellers/onboard/complete", async (req, res) => { const { userId } = req.query; const seller = await db.sellers.findById(userId); const onboarded = await isSellerOnboarded(seller.stripeAccountId); if (onboarded) { await db.sellers.update(userId, { status: "active" }); res.redirect("/dashboard?onboarded=true"); } else { // Seller didn't finish — show completion prompt res.redirect("/sellers/onboard/pending"); } });
Charging Buyers
Destination Charges (Platform collects, sends to seller)
// POST /api/payments/charge export async function chargeWithDestination({ amount, // in cents currency = "usd", paymentMethodId, customerId, sellerAccountId, platformFeePercent = 15, }: { amount: number; currency?: string; paymentMethodId: string; customerId: string; sellerAccountId: string; platformFeePercent?: number; }) { const platformFee = Math.round(amount * (platformFeePercent / 100)); const paymentIntent = await stripe.paymentIntents.create({ amount, currency, customer: customerId, payment_method: paymentMethodId, confirm: true, transfer_data: { destination: sellerAccountId, // Route net to seller }, application_fee_amount: platformFee, // Platform keeps this automatic_payment_methods: { enabled: true, allow_redirects: "never" }, }); return paymentIntent; }
Direct Charges (Seller's Stripe account)
// Charge appears on seller's Stripe dashboard; platform gets fee export async function directCharge({ amount, paymentMethodId, sellerAccountId, platformFeePercent = 10, }: { amount: number; paymentMethodId: string; sellerAccountId: string; platformFeePercent?: number; }) { const platformFee = Math.round(amount * (platformFeePercent / 100)); const paymentIntent = await stripe.paymentIntents.create( { amount, currency: "usd", payment_method: paymentMethodId, confirm: true, application_fee_amount: platformFee, }, { stripeAccount: sellerAccountId, // Create on behalf of seller } ); return paymentIntent; }
Separate Charges + Transfers (most flexible)
// 1. Charge buyer on platform account const paymentIntent = await stripe.paymentIntents.create({ amount: 10000, // $100 currency: "usd", payment_method: paymentMethodId, confirm: true, }); // 2. Later: transfer to seller (e.g., after service delivered) export async function payoutToSeller( paymentIntentId: string, sellerAccountId: string, amount: number // amount to send seller (after platform fee) ) { const transfer = await stripe.transfers.create({ amount, currency: "usd", destination: sellerAccountId, source_transaction: paymentIntentId, // Links transfer to original charge }); return transfer; }
Payouts
Automatic payouts (default)
By default, Stripe automatically pays out to sellers based on their payout schedule. No extra code needed.
// Check payout schedule for a connected account const account = await stripe.accounts.retrieve(sellerAccountId); console.log(account.settings?.payouts?.schedule); // { interval: "daily" | "weekly" | "monthly", ... }
Manual / triggered payouts
// Trigger an instant payout (seller must have instant payouts enabled) export async function triggerPayout(sellerAccountId: string, amount: number) { const payout = await stripe.payouts.create( { amount, currency: "usd", method: "instant", // or "standard" }, { stripeAccount: sellerAccountId, } ); return payout; }
Check seller balance
export async function getSellerBalance(sellerAccountId: string) { const balance = await stripe.balance.retrieve({ stripeAccount: sellerAccountId, }); return { available: balance.available[0]?.amount ?? 0, pending: balance.pending[0]?.amount ?? 0, }; }
Webhooks for Connect
Connect webhooks can fire for your platform account or for events on connected accounts.
// POST /webhooks/stripe import { stripe } from "@/lib/stripe"; app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => { const sig = req.headers["stripe-signature"] as string; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err) { return res.status(400).send(`Webhook Error: ${(err as Error).message}`); } // For Connect events, check event.account const connectedAccountId = (event as any).account as string | undefined; switch (event.type) { // Connected account completed onboarding case "account.updated": { const account = event.data.object as Stripe.Account; if (account.details_submitted) { await db.sellers.update( { stripeAccountId: account.id }, { status: "active" } ); } break; } // Payment succeeded case "payment_intent.succeeded": { const pi = event.data.object as Stripe.PaymentIntent; await db.orders.update( { stripePaymentIntentId: pi.id }, { status: "paid" } ); break; } // Payment failed case "payment_intent.payment_failed": { const pi = event.data.object as Stripe.PaymentIntent; await db.orders.update( { stripePaymentIntentId: pi.id }, { status: "failed", failureReason: pi.last_payment_error?.message } ); break; } // Transfer to seller completed case "transfer.created": { const transfer = event.data.object as Stripe.Transfer; console.log(`Transferred ${transfer.amount} to ${transfer.destination}`); break; } // Payout to seller's bank case "payout.paid": { const payout = event.data.object as Stripe.Payout; console.log(`Payout ${payout.id} paid to ${connectedAccountId}`); break; } // Dispute opened case "charge.dispute.created": { const dispute = event.data.object as Stripe.Dispute; await handleDispute(dispute); break; } } res.json({ received: true }); });
Listen to connected account events
// To receive events from connected accounts, configure in Stripe Dashboard: // Dashboard → Developers → Webhooks → Add endpoint // Check "Listen to events on Connected accounts" // Or via API: const webhookEndpoint = await stripe.webhookEndpoints.create({ url: "https://myapp.com/webhooks/stripe", enabled_events: [ "account.updated", "payment_intent.succeeded", "payment_intent.payment_failed", "transfer.created", "payout.paid", "charge.dispute.created", ], connect: true, // Receive Connect events });
Refunds
// Refund a payment (from platform to buyer) export async function refundPayment( paymentIntentId: string, amount?: number, // omit for full refund reason?: "duplicate" | "fraudulent" | "requested_by_customer" ) { const refund = await stripe.refunds.create({ payment_intent: paymentIntentId, ...(amount && { amount }), ...(reason && { reason }), refund_application_fee: true, // Refund your platform fee too reverse_transfer: true, // Reverse transfer to seller }); return refund; }
OAuth for Standard Accounts
// 1. Redirect seller to Stripe OAuth export function getStripeOAuthUrl(state: string) { return `https://connect.stripe.com/oauth/authorize?` + `response_type=code&client_id=${process.env.STRIPE_CLIENT_ID}` + `&scope=read_write&state=${state}`; } // 2. Handle OAuth callback app.get("/auth/stripe/callback", async (req, res) => { const { code, state } = req.query; const response = await stripe.oauth.token({ grant_type: "authorization_code", code: code as string, }); const connectedAccountId = response.stripe_user_id!; // Save connectedAccountId to seller record res.redirect("/dashboard"); });
Testing
# Use test mode keys (sk_test_...) # Test card numbers: # 4242 4242 4242 4242 — success # 4000 0000 0000 9995 — insufficient funds # 4000 0025 6000 0001 — requires authentication (3DS) # Trigger webhooks locally: stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger payment_intent.succeeded
Environment Variables
STRIPE_SECRET_KEY=sk_test_... STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_CLIENT_ID=ca_... # Only for Standard OAuth BASE_URL=http://localhost:3000