
stripe-connect
by anton-abyzov
Autonomous AI Development Framework. Build production software with specs, tests, and docs that write themselves. Works with Claude, Cursor, Copilot.
SKILL.md
name: stripe-connect description: Implement Stripe Connect for marketplace and platform payments. Handles Direct Charge, Destination Charge, and Separate Charges & Transfers patterns. Covers Connect webhook setup, onboarding flows, payout management, and platform fees. Activates for Stripe Connect, marketplace payments, platform payments, Direct Charge, Destination Charge, connected account, onboarding, Express account, Standard account, Custom account, platform fee, application fee, transfer, payout, account.updated, Connect webhook, multi-party payments, seller payments, vendor payments, service provider payments, two-sided marketplace, booking platform, gig economy, rental platform.
Stripe Connect Integration
Master Stripe Connect for marketplace and platform payments with proper webhook handling, charge patterns, and Connect account management.
When to Use This Skill
- Building a marketplace where sellers receive payments
- Implementing platform payments with fees
- Onboarding vendors/sellers to your platform
- Setting up multi-party payment flows
- Handling payouts to connected accounts
- Managing Connect webhooks (especially for Direct Charge!)
⚠️ Critical: Charge Type Selection
| Pattern | Who Creates Charge | Webhook Location | Best For |
|---|---|---|---|
| Direct Charge | Connected Account | Connect endpoint | Marketplaces where seller owns customer relationship |
| Destination Charge | Platform | Platform endpoint | Platform controls experience, takes fee |
| Separate Charges & Transfers | Platform | Platform endpoint | Maximum flexibility, complex splits |
The #1 Connect Gotcha: Direct Charge Webhook Gap
When using Direct Charge, checkout sessions are created ON the Connected Account, NOT the platform!
❌ Platform webhook only - PAYMENTS WILL BE MISSED!
/webhooks/stripe → Does NOT receive Direct Charge checkout.session.completed
✅ Both webhooks required:
/webhooks/stripe → Platform events (account.updated, etc.)
/webhooks/stripe/connect → checkout.session.completed for Direct Charges!
Quick Start: Direct Charge with Correct Webhooks
1. Create Checkout Session (Direct Charge)
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
async function createDirectChargeCheckout(
connectedAccountId: string,
amount: number,
platformFee: number,
metadata: Record<string, string>
) {
const session = await stripe.checkout.sessions.create(
{
mode: 'payment',
line_items: [{
price_data: {
currency: 'usd',
product_data: { name: 'Service Booking' },
unit_amount: amount,
},
quantity: 1,
}],
payment_intent_data: {
application_fee_amount: platformFee, // Platform takes this
metadata,
},
success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/cancel`,
metadata,
},
{
stripeAccount: connectedAccountId, // CRITICAL: Creates on connected account!
}
);
return session;
}
2. Platform Webhook Endpoint
// /webhooks/stripe - Platform events only
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature']!;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case 'account.updated':
await handleAccountUpdated(event.data.object);
break;
// Note: checkout.session.completed does NOT come here for Direct Charge!
}
res.json({ received: true });
}
);
3. Connect Webhook Endpoint (CRITICAL for Direct Charge!)
// /webhooks/stripe/connect - Connected account events
app.post('/webhooks/stripe/connect',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature']!;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_CONNECT_WEBHOOK_SECRET! // Different secret!
);
const connectedAccountId = event.account;
switch (event.type) {
case 'checkout.session.completed':
// THIS is where Direct Charge payments complete!
await handleConnectCheckoutComplete(event.data.object, connectedAccountId);
break;
case 'checkout.session.expired':
await handleConnectCheckoutExpired(event.data.object, connectedAccountId);
break;
case 'payout.paid':
await handlePayoutPaid(event.data.object, connectedAccountId);
break;
case 'payout.failed':
await handlePayoutFailed(event.data.object, connectedAccountId);
break;
}
res.json({ received: true });
}
);
async function handleConnectCheckoutComplete(
session: Stripe.Checkout.Session,
connectedAccountId: string
) {
// Retrieve full session from the connected account
const fullSession = await stripe.checkout.sessions.retrieve(
session.id,
{ expand: ['line_items', 'payment_intent'] },
{ stripeAccount: connectedAccountId } // CRITICAL!
);
// Idempotent confirmation
await confirmPayment(fullSession.id);
}
4. Stripe Dashboard Setup
-
Platform webhook: Developers → Webhooks → Add endpoint
- URL:
https://yourdomain.com/webhooks/stripe - Select: "Account" events
- Events:
account.updated
- URL:
-
Connect webhook: Developers → Webhooks → Add endpoint
- URL:
https://yourdomain.com/webhooks/stripe/connect - Select: "Connected accounts" (NOT "Account"!)
- Events:
checkout.session.completed,checkout.session.expired,payout.paid,payout.failed
- URL:
Account Onboarding
Express Account (Recommended for Most Cases)
async function createConnectAccount(email: string, businessType: string) {
const account = await stripe.accounts.create({
type: 'express',
email,
business_type: businessType,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
metadata: {
internal_user_id: 'user_123',
},
});
return account;
}
async function createOnboardingLink(accountId: string) {
const accountLink = await stripe.accountLinks.create({
account: accountId,
refresh_url: `${process.env.APP_URL}/onboarding/refresh`,
return_url: `${process.env.APP_URL}/onboarding/complete`,
type: 'account_onboarding',
});
return accountLink.url; // Redirect user here
}
Checking Account Status
async function isAccountReady(accountId: string): Promise<boolean> {
const account = await stripe.accounts.retrieve(accountId);
return (
account.charges_enabled &&
account.payouts_enabled &&
!account.requirements?.currently_due?.length
);
}
Destination Charge Pattern
Use when platform controls the customer relationship:
async function createDestinationCharge(
amount: number,
destinationAccountId: string,
platformFee: number
) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
transfer_data: {
destination: destinationAccountId,
},
application_fee_amount: platformFee,
metadata: {
booking_id: 'booking_123',
},
});
return paymentIntent;
}
Separate Charges & Transfers
For maximum flexibility (e.g., split payments to multiple sellers):
async function chargeAndTransfer(
amount: number,
transfers: Array<{ accountId: string; amount: number }>
) {
// 1. Create charge on platform
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
metadata: { type: 'multi_transfer' },
});
// 2. After payment succeeds, create transfers
// (Usually in webhook handler)
for (const transfer of transfers) {
await stripe.transfers.create({
amount: transfer.amount,
currency: 'usd',
destination: transfer.accountId,
source_transaction: paymentIntent.latest_charge as string,
});
}
}
Handling Refunds with Connect
async function refundDirectCharge(
paymentIntentId: string,
connectedAccountId: string,
refundApplicationFee: boolean = false
) {
const refund = await stripe.refunds.create(
{
payment_intent: paymentIntentId,
refund_application_fee: refundApplicationFee, // Return platform fee too?
},
{
stripeAccount: connectedAccountId, // CRITICAL for Direct Charge!
}
);
return refund;
}
Pre-Implementation Checklist
Webhook Setup
- Platform webhook configured for
account.updated - Connect webhook configured for
checkout.session.completed - Connect webhook configured for
checkout.session.expired - Connect webhook configured for
payout.paid,payout.failed - Two different webhook secrets stored in env vars
- Stripe Dashboard shows "Connected accounts" for Connect webhook
Account Onboarding
- Account creation flow with proper type (Express/Standard/Custom)
- Onboarding link generation
- Return/refresh URL handling
- Account status checking before allowing charges
Charge Flow
- Decided on charge pattern (Direct/Destination/Separate)
- Platform fee calculation implemented
- Idempotent payment confirmation
- 100% promo code handling (if applicable)
Testing
- Test onboarding with test account
- Test checkout with Stripe CLI forwarding
- Test Connect webhook receives
checkout.session.completed - Test refund flow
- Test payout events
Common Mistakes
- Missing Connect webhook for Direct Charge - #1 cause of "payments not working"
- Same webhook secret for both endpoints - They MUST be different
- Not specifying stripeAccount when retrieving session - Gets wrong data
- Assuming account is ready without checking - Always verify
charges_enabled - Hardcoding platform fee - Should be configurable per transaction
Resources
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon


