Implementing Marketplace Payment Processing: Complete Technical Guide
Implement split payments, escrow, and compliance in your marketplace. Learn Stripe Connect setup, webhook handling, dispute resolution, and security best practices with production-ready code examples.
Who Is This For?
This guide is specifically designed for:
Startup Stage:
Building your minimum viable product and preparing for market launch.
Best For Role:
Technical implementation guides and code examples for developers.
Expected Impact:
Medium-term initiatives that build competitive advantages.
What You'll Learn
- Implement Stripe Connect for marketplace split payments
- Configure connected accounts with proper KYC/AML compliance
- Build webhook handlers for real-time payment synchronization
- Handle refunds, disputes, and failed transfers correctly
- Secure payment processing with PCI DSS compliance
Prerequisites
- •Basic understanding of payment processing concepts
- •Familiarity with TypeScript/Node.js
- •Understanding of async/await patterns
- •Knowledge of database transactions
Payment processing is one of the most critical—and complex—components of any marketplace. Unlike traditional e-commerce, marketplaces need to split payments between sellers and the platform while ensuring compliance, security, and a smooth user experience.
This guide provides production-ready code and architectural patterns for implementing marketplace payments with Stripe Connect.
Understanding Marketplace Payment Architecture
Marketplaces have unique payment requirements that traditional payment gateways can't handle alone.
The Split Payment Challenge
When a buyer purchases from a seller on your marketplace, the payment needs to:
- •Charge the buyer for the full amount
- •Collect platform fees (your commission)
- •Transfer to the seller the remaining amount
- •Handle refunds with proper fee adjustments
- •Manage payouts on a schedule (daily, weekly, etc.)
Payment Flow Architecture
Buyer Payment ($100)
↓
Platform Charges ($100)
↓
Platform Fee ($10-20) → Platform Account
↓
Seller Payout ($80-90) → Seller Account
Choosing the Right Payment Provider
For marketplaces, Stripe Connect is the industry standard. Here's why:
Stripe Connect Advantages
- •Split payments: Automatic fee distribution
- •Compliance handling: KYC/AML built-in
- •Global support: 135+ currencies, 45+ countries
- •Flexible fee structures: Application fees or direct charges
- •Payout management: Automated or manual scheduling
- •Dashboard access: Sellers can view their earnings
- •Robust webhooks: Real-time payment updates
Alternative Providers to Consider
PayPal for Marketplaces:
- •Good for global reach and buyer trust
- •Higher fees (3.9% + $0.30)
- •Less flexible than Stripe Connect
Adyen for Platforms:
- •Enterprise-grade solution
- •Better for very high volume ($10M+ monthly)
- •More complex integration
Mangopay:
- •European-focused
- •Strong regulatory compliance
- •Limited to Europe
Recommendation: Use Stripe Connect for 90% of use cases. Only deviate if you have specific regional requirements or enterprise volume needs.
Implementing Stripe Connect
Let's walk through a complete implementation with production-ready code.
1. Create Connected Accounts for Sellers
When a seller signs up, create a connected account:
import Stripe from "stripe";
import { db } from "@/lib/database";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
typescript: true,
});
interface Seller {
id: string;
email: string;
country: string;
firstName: string;
lastName: string;
}
export async function createConnectedAccount(seller: Seller) {
// Create Stripe Connect account
const account = await stripe.accounts.create({
type: "express", // 'express' for easier onboarding, 'standard' for more control
country: seller.country,
email: seller.email,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
business_type: "individual", // or 'company' for businesses
metadata: {
seller_id: seller.id,
marketplace: "your-marketplace-name",
created_source: "seller_registration",
},
});
// Save account ID to your database
await db.sellers.update({
where: { id: seller.id },
data: {
stripe_account_id: account.id,
stripe_onboarding_completed: false,
stripe_charges_enabled: false,
stripe_payouts_enabled: false,
},
});
// Generate onboarding link for seller to complete verification
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: `${process.env.APP_URL}/sellers/onboarding/refresh`,
return_url: `${process.env.APP_URL}/sellers/dashboard`,
type: "account_onboarding",
});
return {
accountId: account.id,
onboardingUrl: accountLink.url,
};
}
Key decisions:
- •
Express vs Standard accounts:
- •Express: Easier onboarding, Stripe handles compliance UI
- •Standard: More control, seller manages own Stripe dashboard
- •Most marketplaces use Express
- •
Business type:
- •Individual: Faster onboarding, suitable for freelancers
- •Company: Required for businesses, more verification needed
2. Monitor Onboarding Status
Check if sellers have completed verification:
export async function checkOnboardingStatus(stripeAccountId: string) {
const account = await stripe.accounts.retrieve(stripeAccountId);
const status = {
detailsSubmitted: account.details_submitted || false,
chargesEnabled: account.charges_enabled || false,
payoutsEnabled: account.payouts_enabled || false,
requirementsCurrentlyDue: account.requirements?.currently_due || [],
requirementsEventuallyDue: account.requirements?.eventually_due || [],
requirementsPastDue: account.requirements?.past_due || [],
};
// Update database with current status
await db.sellers.update({
where: { stripe_account_id: stripeAccountId },
data: {
stripe_onboarding_completed: status.detailsSubmitted,
stripe_charges_enabled: status.chargesEnabled,
stripe_payouts_enabled: status.payoutsEnabled,
},
});
return status;
}
3. Process Split Payments
Charge the buyer and automatically split to the seller:
interface Order {
id: string;
amount: number; // in cents
currency: string;
sellerId: string;
buyerId: string;
description: string;
}
export async function processMarketplacePayment(
order: Order,
paymentMethodId: string,
) {
// Get seller's Stripe account
const seller = await db.sellers.findUnique({
where: { id: order.sellerId },
select: { stripe_account_id: true, stripe_charges_enabled: true },
});
if (!seller?.stripe_account_id || !seller.stripe_charges_enabled) {
throw new Error("Seller is not ready to accept payments");
}
// Calculate platform fee (e.g., 10% + $0.30)
const platformFee = calculatePlatformFee(order.amount);
// Create payment intent with split
const paymentIntent = await stripe.paymentIntents.create({
amount: order.amount,
currency: order.currency,
payment_method: paymentMethodId,
confirm: true, // Immediately confirm the payment
application_fee_amount: platformFee,
transfer_data: {
destination: seller.stripe_account_id,
},
metadata: {
order_id: order.id,
seller_id: order.sellerId,
buyer_id: order.buyerId,
},
description: order.description,
});
// Save payment record to database
await db.payments.create({
data: {
order_id: order.id,
payment_intent_id: paymentIntent.id,
amount_cents: order.amount,
platform_fee_cents: platformFee,
status: paymentIntent.status,
stripe_account_id: seller.stripe_account_id,
},
});
return paymentIntent;
}
function calculatePlatformFee(amountCents: number): number {
// 10% commission + $0.30 fixed fee
const percentageFee = Math.round(amountCents * 0.1);
const fixedFee = 30; // $0.30 in cents
return percentageFee + fixedFee;
}
Alternative: Separate Charge + Transfer (for Escrow)
If you need to hold funds before transferring to seller (e.g., after delivery confirmation):
export async function createEscrowPayment(
order: Order,
paymentMethodId: string,
) {
// Charge buyer (funds held on platform account)
const paymentIntent = await stripe.paymentIntents.create({
amount: order.amount,
currency: order.currency,
payment_method: paymentMethodId,
confirm: true,
metadata: {
order_id: order.id,
seller_id: order.sellerId,
buyer_id: order.buyerId,
escrow: "true",
},
});
return paymentIntent;
}
export async function releaseEscrowFunds(orderId: string) {
const payment = await db.payments.findUnique({
where: { order_id: orderId },
include: { order: { include: { seller: true } } },
});
if (!payment?.payment_intent_id) {
throw new Error("Payment not found");
}
const platformFee = calculatePlatformFee(payment.amount_cents);
const sellerAmount = payment.amount_cents - platformFee;
// Transfer to seller
const transfer = await stripe.transfers.create({
amount: sellerAmount,
currency: payment.order.currency,
destination: payment.order.seller.stripe_account_id!,
metadata: {
order_id: orderId,
payment_intent_id: payment.payment_intent_id,
},
});
// Update payment record
await db.payments.update({
where: { id: payment.id },
data: {
transfer_id: transfer.id,
status: "transferred",
transferred_at: new Date(),
},
});
return transfer;
}
4. Handle Refunds Correctly
Refunds need to adjust platform fees appropriately:
export async function refundMarketplaceOrder(orderId: string, reason?: string) {
const order = await db.orders.findUnique({
where: { id: orderId },
include: { payment: true, seller: true },
});
if (!order?.payment?.payment_intent_id) {
throw new Error("Payment intent not found");
}
// Refund reverses the application fee automatically
const refund = await stripe.refunds.create({
payment_intent: order.payment.payment_intent_id,
reverse_transfer: true, // Returns funds from connected account
metadata: {
order_id: orderId,
reason: reason || "customer_request",
},
});
// Update order status
await db.orders.update({
where: { id: orderId },
data: {
status: "refunded",
refunded_at: new Date(),
refund_reason: reason,
},
});
// Update payment record
await db.payments.update({
where: { id: order.payment.id },
data: {
refund_id: refund.id,
status: "refunded",
},
});
return refund;
}
// Partial refund
export async function partialRefund(
orderId: string,
refundAmountCents: number,
reason?: string,
) {
const order = await db.orders.findUnique({
where: { id: orderId },
include: { payment: true },
});
if (!order?.payment?.payment_intent_id) {
throw new Error("Payment not found");
}
// Calculate proportional platform fee refund
const originalAmount = order.payment.amount_cents;
const originalPlatformFee = order.payment.platform_fee_cents;
const platformFeeRefund = Math.round(
(refundAmountCents / originalAmount) * originalPlatformFee,
);
const refund = await stripe.refunds.create({
payment_intent: order.payment.payment_intent_id,
amount: refundAmountCents,
reverse_transfer: true,
refund_application_fee: true, // Refund platform fee proportionally
metadata: {
order_id: orderId,
reason: reason || "partial_refund",
},
});
return refund;
}
Handling Edge Cases
Real-world payment processing requires handling various failure scenarios.
Disputed Payments
Implement a dispute handling system:
// Webhook handler for disputes
export async function handleDispute(dispute: Stripe.Dispute) {
const order = await db.orders.findFirst({
where: { payment_intent_id: dispute.payment_intent as string },
include: { seller: true, buyer: true },
});
if (!order) {
console.error("Order not found for disputed payment:", dispute.id);
return;
}
// Create dispute record
await db.disputes.create({
data: {
order_id: order.id,
stripe_dispute_id: dispute.id,
amount_cents: dispute.amount,
reason: dispute.reason,
status: dispute.status,
evidence_due_by: new Date(dispute.evidence_details?.due_by! * 1000),
},
});
// Notify seller
await sendEmail({
to: order.seller.email,
subject: `Payment Dispute: Order #${order.id}`,
template: "dispute-notification",
data: {
orderNumber: order.id,
disputeReason: dispute.reason,
amount: (dispute.amount / 100).toFixed(2),
evidenceDueBy: new Date(dispute.evidence_details?.due_by! * 1000),
disputeDashboardUrl: `${process.env.APP_URL}/sellers/disputes/${dispute.id}`,
},
});
// Update order status
await db.orders.update({
where: { id: order.id },
data: { status: "disputed" },
});
}
// Submit evidence for dispute
export async function submitDisputeEvidence(
disputeId: string,
evidence: {
customerName?: string;
customerEmailAddress?: string;
customerPurchaseIp?: string;
billingAddress?: string;
receipt?: string;
shippingDocumentation?: string;
customerSignature?: string;
uncategorizedText?: string;
},
) {
const dispute = await stripe.disputes.update(disputeId, {
evidence,
submit: true,
});
await db.disputes.update({
where: { stripe_dispute_id: disputeId },
data: {
status: dispute.status,
evidence_submitted_at: new Date(),
},
});
return dispute;
}
Failed Transfers
Handle scenarios where transfers to sellers fail:
export async function handleFailedTransfer(transfer: Stripe.Transfer) {
// Log the failure
await db.failedTransfers.create({
data: {
transfer_id: transfer.id,
seller_stripe_account: transfer.destination as string,
amount_cents: transfer.amount,
failure_code: transfer.failure_code,
failure_message: transfer.failure_message,
order_id: transfer.metadata.order_id,
},
});
// Notify admin for manual review
await notifyAdmin("Failed Transfer Alert", {
transferId: transfer.id,
amount: transfer.amount / 100,
seller: transfer.destination,
reason: transfer.failure_message,
});
// Update order status
if (transfer.metadata.order_id) {
await db.orders.update({
where: { id: transfer.metadata.order_id },
data: { status: "transfer_failed" },
});
}
}
// Retry failed transfer
export async function retryFailedTransfer(transferId: string) {
const failedTransfer = await db.failedTransfers.findUnique({
where: { transfer_id: transferId },
include: { order: { include: { seller: true } } },
});
if (!failedTransfer?.order) {
throw new Error("Failed transfer or order not found");
}
try {
// Attempt new transfer
const transfer = await stripe.transfers.create({
amount: failedTransfer.amount_cents,
currency: "usd",
destination: failedTransfer.order.seller.stripe_account_id!,
metadata: {
order_id: failedTransfer.order_id,
retry_of: transferId,
},
});
// Mark as resolved
await db.failedTransfers.update({
where: { id: failedTransfer.id },
data: { resolved: true, retry_transfer_id: transfer.id },
});
return transfer;
} catch (error) {
console.error("Retry failed:", error);
throw error;
}
}
Compliance and Security
Payment processing comes with significant compliance requirements.
PCI DSS Compliance Checklist
Stripe handles most PCI compliance, but you must:
- •Never store raw credit card numbers, CVV/CVC codes, or track data
- •Use Stripe Elements for card collection (keeps card data off your servers)
- •Implement HTTPS everywhere (enforce with HSTS headers)
- •Log access to payment data (audit trail)
- •Validate webhook signatures to prevent injection attacks
- •Use environment variables for API keys (never commit to code)
Implementation:
// ❌ NEVER DO THIS - PCI violation
const cardNumber = request.body.cardNumber;
await db.payment.create({
data: { cardNumber }, // ILLEGAL
});
// ✅ CORRECT: Use Stripe tokens
export async function POST(request: Request) {
const { paymentMethodId, amount, orderId } = await request.json();
// Stripe handles the card data - you only store IDs
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: "usd",
payment_method: paymentMethodId,
confirm: true,
});
// You only store Stripe IDs - safe to store
await db.payment.create({
data: {
order_id: orderId,
payment_intent_id: paymentIntent.id, // Safe
amount_cents: amount,
},
});
}
KYC/AML Requirements
Connected accounts must complete verification:
export async function checkAccountVerification(accountId: string) {
const account = await stripe.accounts.retrieve(accountId);
const requirements = {
currentlyDue: account.requirements?.currently_due || [],
eventuallyDue: account.requirements?.eventually_due || [],
pastDue: account.requirements?.past_due || [],
disabled: account.requirements?.disabled_reason,
};
// Account is restricted if there are past due requirements
const isRestricted = requirements.pastDue.length > 0;
if (isRestricted) {
// Disable seller from creating new listings
await db.sellers.update({
where: { stripe_account_id: accountId },
data: {
account_restricted: true,
restriction_reason: account.requirements?.disabled_reason,
},
});
// Notify seller to complete verification
await notifySeller(account.metadata.seller_id, {
type: "verification_required",
requirements: requirements.pastDue,
action_url: await createAccountUpdateLink(accountId),
});
}
return {
isVerified: account.details_submitted,
isRestricted,
requirements,
};
}
async function createAccountUpdateLink(accountId: string) {
const accountLink = await stripe.accountLinks.create({
account: accountId,
refresh_url: `${process.env.APP_URL}/sellers/verification/refresh`,
return_url: `${process.env.APP_URL}/sellers/dashboard`,
type: "account_update",
});
return accountLink.url;
}
Payout Management
Control when and how sellers receive payments.
Configure Payout Schedules
export async function updatePayoutSchedule(
accountId: string,
schedule: "daily" | "weekly" | "monthly" | "manual",
) {
const settings: Stripe.AccountUpdateParams.Settings.Payouts.Schedule = {
interval: schedule === "manual" ? "manual" : schedule,
};
// Add specific day for weekly/monthly
if (schedule === "weekly") {
settings.weekly_anchor = "friday";
}
if (schedule === "monthly") {
settings.monthly_anchor = 1; // 1st of month
}
await stripe.accounts.update(accountId, {
settings: {
payouts: {
schedule: settings,
},
},
});
}
Manual Payouts for High-Value Transactions
export async function createManualPayout(
sellerId: string,
amountCents: number,
description?: string,
) {
const seller = await db.sellers.findUnique({
where: { id: sellerId },
});
if (!seller?.stripe_account_id) {
throw new Error("Seller not connected to Stripe");
}
// Create payout on seller's connected account
const payout = await stripe.payouts.create(
{
amount: amountCents,
currency: "usd",
description: description || `Manual payout to ${seller.business_name}`,
metadata: {
seller_id: sellerId,
payout_type: "manual",
},
},
{
stripeAccount: seller.stripe_account_id,
},
);
// Record payout in database
await db.payouts.create({
data: {
seller_id: sellerId,
stripe_payout_id: payout.id,
amount_cents: amountCents,
status: payout.status,
},
});
return payout;
}
Webhook Implementation
Webhooks keep your system in sync with Stripe events in real-time.
Complete Webhook Handler
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { stripe } from "@/lib/stripe";
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json(
{ error: "Missing stripe-signature header" },
{ status: 400 },
);
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err) {
console.error("Webhook signature verification failed:", err);
return NextResponse.json(
{ error: "Webhook signature verification failed" },
{ status: 400 },
);
}
// Handle different event types
try {
switch (event.type) {
case "payment_intent.succeeded":
await handlePaymentSuccess(event.data.object);
break;
case "payment_intent.payment_failed":
await handlePaymentFailure(event.data.object);
break;
case "charge.refunded":
await handleRefund(event.data.object);
break;
case "charge.dispute.created":
await handleDispute(event.data.object);
break;
case "account.updated":
await handleAccountUpdate(event.data.object);
break;
case "transfer.failed":
await handleFailedTransfer(event.data.object);
break;
case "payout.paid":
await handlePayoutPaid(event.data.object);
break;
case "payout.failed":
await handlePayoutFailed(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true }, { status: 200 });
} catch (error) {
console.error("Error processing webhook:", error);
return NextResponse.json(
{ error: "Webhook processing failed" },
{ status: 500 },
);
}
}
async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
await db.payments.update({
where: { payment_intent_id: paymentIntent.id },
data: { status: "succeeded" },
});
// Update order status
const payment = await db.payments.findUnique({
where: { payment_intent_id: paymentIntent.id },
});
if (payment?.order_id) {
await db.orders.update({
where: { id: payment.order_id },
data: { status: "paid", paid_at: new Date() },
});
}
}
async function handlePaymentFailure(paymentIntent: Stripe.PaymentIntent) {
await db.payments.update({
where: { payment_intent_id: paymentIntent.id },
data: {
status: "failed",
failure_code: paymentIntent.last_payment_error?.code,
failure_message: paymentIntent.last_payment_error?.message,
},
});
// Notify buyer
const payment = await db.payments.findUnique({
where: { payment_intent_id: paymentIntent.id },
include: { order: { include: { buyer: true } } },
});
if (payment?.order?.buyer) {
await sendEmail({
to: payment.order.buyer.email,
subject: "Payment Failed",
template: "payment-failed",
data: {
orderId: payment.order_id,
reason: paymentIntent.last_payment_error?.message,
},
});
}
}
async function handleAccountUpdate(account: Stripe.Account) {
const seller = await db.sellers.findFirst({
where: { stripe_account_id: account.id },
});
if (!seller) return;
await checkAccountVerification(account.id);
}
async function handlePayoutPaid(payout: Stripe.Payout) {
await db.payouts.update({
where: { stripe_payout_id: payout.id },
data: {
status: "paid",
paid_at: new Date(payout.arrival_date * 1000),
},
});
}
async function handlePayoutFailed(payout: Stripe.Payout) {
await db.payouts.update({
where: { stripe_payout_id: payout.id },
data: {
status: "failed",
failure_code: payout.failure_code,
failure_message: payout.failure_message,
},
});
// Notify seller
const payoutRecord = await db.payouts.findUnique({
where: { stripe_payout_id: payout.id },
include: { seller: true },
});
if (payoutRecord?.seller) {
await notifySeller(payoutRecord.seller_id, {
type: "payout_failed",
amount: payout.amount / 100,
reason: payout.failure_message,
});
}
}
Testing Strategy
Thorough testing is essential for payment systems.
Stripe Test Card Numbers
export const testCards = {
success: "4242424242424242",
decline: "4000000000000002",
insufficientFunds: "4000000000009995",
requiresAuthentication: "4000002500003155", // 3D Secure
expiredCard: "4000000000000069",
incorrectCVC: "4000000000000127",
processingError: "4000000000000119",
};
// Test in different countries
export const testCardsByCountry = {
US: "4242424242424242",
UK: "4000008260000000",
CA: "4000001240000000",
AU: "4000000360000000",
};
Integration Test Examples
// tests/payments.test.ts
import { describe, it, expect } from "vitest";
describe("Marketplace Payments", () => {
it("should split payment correctly", async () => {
const order = await createTestOrder({ amount: 10000 }); // $100
const payment = await processMarketplacePayment(
order,
"pm_card_visa", // Test payment method
);
expect(payment.amount).toBe(10000);
expect(payment.application_fee_amount).toBe(1030); // 10% + $0.30
});
it("should handle refunds with fee reversal", async () => {
const order = await createTestOrderWithPayment();
const refund = await refundMarketplaceOrder(order.id);
expect(refund.status).toBe("succeeded");
expect(refund.amount).toBe(order.amount);
// Verify order status updated
const updatedOrder = await db.orders.findUnique({
where: { id: order.id },
});
expect(updatedOrder?.status).toBe("refunded");
});
it("should handle partial refunds correctly", async () => {
const order = await createTestOrderWithPayment({ amount: 10000 });
const refund = await partialRefund(order.id, 5000); // Refund $50
expect(refund.amount).toBe(5000);
});
it("should prevent payments to unverified sellers", async () => {
const unverifiedSeller = await createTestSeller({
stripe_charges_enabled: false,
});
const order = await createTestOrder({ sellerId: unverifiedSeller.id });
await expect(
processMarketplacePayment(order, "pm_card_visa"),
).rejects.toThrow("Seller is not ready to accept payments");
});
it("should handle failed transfers", async () => {
// Create order with invalid seller account
const order = await createTestOrder();
await createEscrowPayment(order, "pm_card_visa");
// Attempt transfer to invalid account
await expect(releaseEscrowFunds(order.id)).rejects.toThrow();
// Verify failure logged
const failedTransfer = await db.failedTransfers.findFirst({
where: { order_id: order.id },
});
expect(failedTransfer).toBeDefined();
});
});
Manual Testing Checklist
## Payment Flow Testing
- [ ] Create connected account successfully
- [ ] Complete onboarding flow (test mode)
- [ ] Process split payment with 10% fee
- [ ] Verify platform fee collected correctly
- [ ] Verify seller receives 90% of payment
- [ ] Process full refund
- [ ] Verify platform fee reversed on refund
- [ ] Process partial refund (50%)
- [ ] Test payment with 3D Secure card
- [ ] Handle declined card gracefully
- [ ] Test insufficient funds error
- [ ] Create escrow payment
- [ ] Release escrow after 7 days
- [ ] Test manual payout creation
- [ ] Verify webhook signature validation
- [ ] Test dispute creation and notification
- [ ] Test failed transfer handling
## Edge Cases
- [ ] Payment to unverified seller (should fail)
- [ ] Refund after transfer completed
- [ ] Dispute after refund issued
- [ ] Multiple rapid payments (rate limiting)
- [ ] Payment in different currencies
- [ ] Seller account disabled during payment
- [ ] Network failure during payment
- [ ] Webhook retry behavior
Production Deployment Checklist
Before going live with payments:
## Pre-Launch Requirements
- [ ] Move from test API keys to live API keys
- [ ] Configure live webhook endpoints
- [ ] Verify webhook signature validation works
- [ ] Set up monitoring for payment failures
- [ ] Configure alerts for failed transfers
- [ ] Test actual bank account payouts (small amount)
- [ ] Verify tax collection if required (Stripe Tax)
- [ ] Review and set payout schedule defaults
- [ ] Configure dispute email notifications
- [ ] Set up Stripe Radar rules for fraud prevention
- [ ] Document refund policy clearly
- [ ] Train support team on payment issues
- [ ] Create runbook for payment failures
- [ ] Set up backup payment processor (optional)
- [ ] Verify PCI compliance checklist complete
## Legal/Compliance
- [ ] Terms of Service include payment terms
- [ ] Privacy Policy covers payment data
- [ ] Seller agreement includes fee structure
- [ ] Refund policy documented
- [ ] Dispute resolution process defined
- [ ] Tax compliance verified (1099-K reporting if US)
- [ ] International seller compliance (if applicable)
Monitoring and Alerting
Set up monitoring for critical payment issues:
// lib/monitoring/payments.ts
export async function monitorPaymentHealth() {
const last24Hours = new Date(Date.now() - 24 * 60 * 60 * 1000);
// Check failed payment rate
const totalPayments = await db.payments.count({
where: { created_at: { gte: last24Hours } },
});
const failedPayments = await db.payments.count({
where: {
created_at: { gte: last24Hours },
status: "failed",
},
});
const failureRate = totalPayments > 0 ? failedPayments / totalPayments : 0;
if (failureRate > 0.05) {
// Alert if >5% failure rate
await alertAdmin({
severity: "high",
message: `High payment failure rate: ${(failureRate * 100).toFixed(1)}%`,
metric: { total: totalPayments, failed: failedPayments },
});
}
// Check for stuck transfers
const stuckTransfers = await db.payments.count({
where: {
created_at: { lt: new Date(Date.now() - 48 * 60 * 60 * 1000) },
status: "succeeded",
transfer_id: null,
},
});
if (stuckTransfers > 0) {
await alertAdmin({
severity: "medium",
message: `${stuckTransfers} payments not transferred after 48 hours`,
});
}
// Check dispute rate
const disputes = await db.disputes.count({
where: { created_at: { gte: last24Hours } },
});
const disputeRate = totalPayments > 0 ? disputes / totalPayments : 0;
if (disputeRate > 0.01) {
// Alert if >1% dispute rate
await alertAdmin({
severity: "medium",
message: `Elevated dispute rate: ${(disputeRate * 100).toFixed(2)}%`,
});
}
}
Common Issues and Solutions
Issue: "Account not found" when creating payment
Solution: Verify seller has completed onboarding and account is active:
const account = await stripe.accounts.retrieve(stripeAccountId);
if (!account.charges_enabled) {
throw new Error("Seller account not ready for payments");
}
Issue: "Transfer failed: insufficient funds"
Solution: Ensure payment intent uses confirm: true or is confirmed before creating transfer. For escrow, wait for payment to clear.
Issue: Webhook signature verification fails
Solution: Ensure you're using raw request body (not parsed JSON):
// ✅ Correct
const body = await request.text();
const event = stripe.webhooks.constructEvent(body, signature, secret);
// ❌ Wrong
const body = await request.json();
const event = stripe.webhooks.constructEvent(body, signature, secret);
Issue: Platform fee calculation errors
Solution: Always calculate fees in cents and round:
const platformFee = Math.round(amountCents * 0.1) + 30;
// NOT: const platformFee = amountCents * 0.10 + 0.30 (floating point errors)
Conclusion
Implementing marketplace payments correctly is critical for business success. Key takeaways:
- •Use Stripe Connect for robust split payment infrastructure
- •Handle compliance through proper KYC/AML verification
- •Implement webhooks for real-time payment synchronization
- •Test thoroughly with all edge cases before launch
- •Monitor closely for payment failures and disputes
- •Plan for refunds and handle them gracefully
- •Secure API keys and validate webhook signatures
Following these patterns will give you production-grade payment processing that scales from MVP to millions in GMV.
Additional Resources
How much should your build actually cost?
Get a personalized investment estimate based on your platform type, scope, and timeline.
Open the Investment CalculatorAbout the Author

Chris Mask
Founder & CEO
Serial entrepreneur, marketplace architect, and AI-assisted development pioneer with 7+ years building two-sided platforms. Founded Directorism after launching and exiting two successful marketplace businesses. Has personally architected and consulted on 200+ marketplace and directory projects. Recognized authority on cold-start problems, platform economics, marketplace SEO, and leveraging AI tools for rapid development. Early adopter of AI-powered coding workflows, integrating Claude, Cursor, and agentic development patterns into production systems.
Related Resources
Marketplace Security: Complete Implementation Guide from Authentication to Compliance
Implement comprehensive security for your marketplace. Learn authentication strategies, authorization patterns, payment security, data encryption, GDPR compliance, and how to prevent common vulnerabilities.
Real-Time Messaging System Architecture for Marketplaces
Learn how to architect pre-booking and post-booking messaging systems with automated sequences and engagement optimization.
Search Technology Implementation Guide for Marketplaces
Learn how to select and implement the right search technology with code examples for PostgreSQL, Typesense, Elasticsearch, and Algolia.