stripe
Implement Stripe Connect payments for PhotoVault marketplace using destination charges. Use when working with checkout sessions, webhook handlers, platform subscriptions ($22/month), commission splits (50/50), Connect onboarding, failed payment handling (dunning), or debugging payment issues. Includes idempotency patterns and PhotoVault product IDs.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
⚠️ MANDATORY WORKFLOW - DO NOT SKIP
When this skill activates, you MUST follow the expert workflow before writing any code:
-
Spawn Domain Expert using the Task tool with this prompt:
Read the expert prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\stripe-expert.md Then research the codebase and write an implementation plan to: docs/claude/plans/stripe-[task-name]-plan.md Task: [describe the user's request] -
Spawn QA Critic after expert returns, using Task tool:
Read the QA critic prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\qa-critic-expert.md Review the plan at: docs/claude/plans/stripe-[task-name]-plan.md Write critique to: docs/claude/plans/stripe-[task-name]-critique.md -
Present BOTH plan and critique to user - wait for approval before implementing
DO NOT read files and start coding. DO NOT rationalize that "this is simple." Follow the workflow.
Stripe Integration
Core Principles
Idempotency is Non-Negotiable
Webhooks can fire multiple times. API calls can timeout and retry. Your code must handle duplicates gracefully.
async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
const existing = await db.payments.findOne({
stripe_payment_intent_id: paymentIntent.id
})
if (existing) {
console.log(`Payment ${paymentIntent.id} already processed, skipping`)
return
}
await db.payments.create({
stripe_payment_intent_id: paymentIntent.id,
amount: paymentIntent.amount,
status: 'completed'
})
}
Use Destination Charges for Marketplaces
PhotoVault uses Stripe Connect with Express accounts. Destination charges:
- Route money directly to photographer
- Deduct platform fee automatically
- Create charge + transfer atomically
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
payment_intent_data: {
application_fee_amount: platformFeeCents, // 50% to PhotoVault
transfer_data: {
destination: photographer.stripe_connect_account_id,
},
},
})
Always Verify Webhook Signatures
const event = stripe.webhooks.constructEvent(
rawBody,
signature,
webhookSecret
)
Anti-Patterns
Webhook Mistakes
Not verifying webhook signatures
// WRONG
const event = JSON.parse(req.body)
// RIGHT
const event = stripe.webhooks.constructEvent(body, sig, secret)
Not handling duplicate events
// WRONG
case 'checkout.session.completed':
await createCommission(session) // Duplicates on retry!
// RIGHT
case 'checkout.session.completed':
const exists = await db.commissions.findOne({
stripe_session_id: session.id
})
if (!exists) {
await createCommission(session)
}
Processing webhooks synchronously
// WRONG: Slow response, timeout risk
app.post('/webhook', async (req, res) => {
await sendEmails()
await updateDatabase()
res.json({ received: true })
})
// RIGHT: Acknowledge fast
app.post('/webhook', async (req, res) => {
await queueForProcessing(event)
res.json({ received: true })
})
Connect Mistakes
Using transfers when you should use destination charges
// WRONG: Two API calls, race condition risk
const charge = await stripe.paymentIntents.create({ amount: 10000 })
const transfer = await stripe.transfers.create({ amount: 5000, destination: accountId })
// RIGHT: Atomic destination charge
const session = await stripe.checkout.sessions.create({
payment_intent_data: {
application_fee_amount: 5000,
transfer_data: { destination: accountId }
}
})
Webhook Handler Pattern
import Stripe from 'stripe'
import { NextRequest, NextResponse } from 'next/server'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: NextRequest) {
const body = await req.text()
const signature = req.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
try {
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object as Stripe.Checkout.Session)
break
case 'invoice.paid':
await handleInvoicePaid(event.data.object as Stripe.Invoice)
break
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object as Stripe.Invoice)
break
}
} catch (err) {
console.error(`Error processing ${event.type}:`, err)
}
return NextResponse.json({ received: true })
}
PhotoVault Configuration
Stripe Products (Test Mode)
| Product | ID | Price |
|---|---|---|
| Year Package | prod_TV5f6EOT5K3wKt |
$100 + $8/mo |
| 6-Month Package | prod_TV5f1eAehZIlA2 |
$50 + $8/mo |
| 6-Month Trial | prod_TV5fYvY8l0WaaV |
$20 one-time |
| Client Monthly | prod_TV5gXyg5nNn635 |
$8/month |
| Direct Monthly | prod_TV6BkuQUCil1ZD |
$8/month (0% commission) |
| Platform Fee | prod_TV5evkNAa2Ezo5 |
$22/month |
Key Files
src/
├── lib/stripe.ts # Stripe config & helpers
├── app/api/
│ ├── stripe/
│ │ ├── create-checkout/ # Checkout session creation
│ │ ├── connect/ # Connect onboarding
│ │ └── platform-subscription/
│ └── webhooks/stripe/ # Webhook handler
Environment Variables
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_PLATFORM_MONTHLY=price_...
Commission Rate
- Platform takes 50% (
PHOTOGRAPHER_COMMISSION_RATE = 0.50) - API Version:
2025-09-30.clover
Testing with Stripe CLI
# Forward webhooks to local server
stripe listen --forward-to localhost:3002/api/webhooks/stripe
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
Test Cards
| Card | Number | Result |
|---|---|---|
| Success | 4242 4242 4242 4242 |
Payment succeeds |
| Decline | 4000 0000 0000 0002 |
Card declined |
| 3D Secure | 4000 0025 0000 3155 |
Requires auth |
| Insufficient | 4000 0000 0000 9995 |
Insufficient funds |
Debugging Checklist
- Check Stripe Dashboard → Payments → Find the payment
- Check webhook logs → Developers → Webhooks → Recent events
- Verify Connect account → Connect → Accounts → Is it enabled?
- Check your logs → Console for errors
- Verify webhook secret → Is it correct for this endpoint?
- Check idempotency → Is the commission already created?
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!