はじめに — なぜ Stripe Connect がマーケットプレイスの標準解なのか
個人開発者やスタートアップがマルチベンダー型のマーケットプレイス(複数の売り手と買い手をつなぐ場)を構築する場合、決済の複雑性が最大のボトルネックになります。売上を各ベンダーに自動分配し、プラットフォーム手数料を徴収し、出金タイミングを制御し、資金移動業の法規制に対応する — これらを自前で実装するのは現実的ではありません。
Stripe Connect はこれらすべてを解決する API です。Antigravity と組み合わせることで、コード補完・テスト生成・エラーデバッグを AI がサポートしながら、本番品質のマーケットプレイス決済システムを効率的に構築できます。
1. Stripe Connect アカウントタイプの選択
3種類のアカウントタイプと使い分け
Stripe Connect には 3 種類のアカウントタイプがあり、プロダクトの性質によって選択が変わります。
Standard アカウント
- ベンダーが自分で Stripe ダッシュボードを管理する
- KYC(本人確認)はStripeが直接対応
- 実装が最も簡単だが、UXのカスタマイズが制限される
- 適したユースケース: プロ向けツール、B2B SaaS の決済連携
Express アカウント
- Stripe が提供するオンボーディングUIを使用
- Stripe ダッシュボードへの限定アクセスを提供
- KYCはStripeが処理、プラットフォームはほぼノータッチ
- マーケットプレイスの場合、最もバランスが良い選択
Custom アカウント
- すべての UI/UX をプラットフォームが制御
- KYC・コンプライアンスもプラットフォームの責任
- 実装コストが最も高い
- 適したユースケース: 金融機関・大規模エンタープライズ
2. プロジェクトセットアップ
依存関係のインストール
npm install stripe @stripe/stripe-js next zod
npm install -D @types/stripe
環境変数
# .env.local
STRIPE_SECRET_KEY=sk_live_YOUR_STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET
STRIPE_PLATFORM_PERCENTAGE=10 # プラットフォーム手数料(10%)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_YOUR_PUBLISHABLE_KEY
Stripe クライアントの初期化
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
typescript: true,
});
// プラットフォーム手数料率
export const PLATFORM_FEE_PERCENTAGE =
parseInt(process.env.STRIPE_PLATFORM_PERCENTAGE || "10", 10);
/**
* アプリケーション手数料を計算する
* @param amount 合計金額(JPY)
* @returns プラットフォーム手数料
*/
export function calculateApplicationFee(amount: number): number {
return Math.floor(amount * PLATFORM_FEE_PERCENTAGE / 100);
}
3. ベンダーオンボーディング(Expressアカウント作成)
Connect アカウント作成 API
// app/api/connect/create-account/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { z } from "zod";
const CreateAccountSchema = z.object({
userId: z.string(),
email: z.string().email(),
country: z.string().length(2).default("JP"),
businessType: z.enum(["individual", "company"]).default("individual"),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = CreateAccountSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid request", details: parsed.error.flatten() },
{ status: 400 }
);
}
const { userId, email, country, businessType } = parsed.data;
try {
// Express アカウントを作成
const account = await stripe.accounts.create({
type: "express",
email,
country,
business_type: businessType,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
settings: {
payouts: {
schedule: {
interval: "weekly", // 週次自動出金
weekly_anchor: "monday",
},
},
},
metadata: {
platform_user_id: userId, // プラットフォーム側のユーザーID
},
});
// データベースに Stripe Account ID を保存
// await db.users.update({ id: userId, stripeAccountId: account.id })
// オンボーディングURLを生成
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: `${process.env.NEXT_PUBLIC_APP_URL}/onboarding/refresh`,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/onboarding/complete`,
type: "account_onboarding",
});
return NextResponse.json({
accountId: account.id,
onboardingUrl: accountLink.url,
});
} catch (error) {
console.error("Stripe account creation error:", error);
return NextResponse.json(
{ error: "Failed to create Connect account" },
{ status: 500 }
);
}
}
オンボーディング状態の確認
// app/api/connect/check-status/route.ts
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const accountId = searchParams.get("accountId");
if (!accountId) {
return NextResponse.json({ error: "accountId required" }, { status: 400 });
}
const account = await stripe.accounts.retrieve(accountId);
const isReady =
account.details_submitted &&
account.charges_enabled &&
account.payouts_enabled;
return NextResponse.json({
isReady,
chargesEnabled: account.charges_enabled,
payoutsEnabled: account.payouts_enabled,
requirements: account.requirements?.currently_due || [],
});
}
4. マーケットプレイス決済フロー
Payment Intent の作成(売上自動分配付き)
// app/api/payments/create-intent/route.ts
import { stripe, calculateApplicationFee } from "@/lib/stripe";
const CreateIntentSchema = z.object({
amount: z.number().int().positive(), // JPY(円)
vendorAccountId: z.string(), // 売り手のStripe Account ID
orderId: z.string(),
description: z.string(),
});
export async function POST(req: NextRequest) {
const { amount, vendorAccountId, orderId, description } =
CreateIntentSchema.parse(await req.json());
const applicationFeeAmount = calculateApplicationFee(amount);
/**
* Destination Charge(デスティネーションチャージ)方式
*
* - 決済はプラットフォームに行われる
* - application_fee_amount 分がプラットフォームの収益
* - 残りは自動的に vendorAccountId に転送される
* - 資金移動業ライセンスが不要(Stripeが取り扱う)
*/
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: "jpy",
application_fee_amount: applicationFeeAmount,
transfer_data: {
destination: vendorAccountId,
},
metadata: {
order_id: orderId,
vendor_account_id: vendorAccountId,
},
description,
automatic_payment_methods: { enabled: true },
});
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
applicationFee: applicationFeeAmount,
vendorAmount: amount - applicationFeeAmount,
});
}
5. Webhook による非同期決済管理
Webhook ハンドラーの実装
// app/api/stripe/webhook/route.ts
import { headers } from "next/headers";
export async function POST(req: Request) {
const body = await req.text();
const signature = 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: "Webhook signature verification failed" },
{ status: 400 }
);
}
switch (event.type) {
case "payment_intent.succeeded": {
const intent = event.data.object as Stripe.PaymentIntent;
const orderId = intent.metadata.order_id;
// 注文を「支払い済み」に更新
await updateOrderStatus(orderId, "paid");
// ベンダーに通知
await notifyVendor(intent.metadata.vendor_account_id, {
amount: intent.amount - (intent.application_fee_amount || 0),
orderId,
});
break;
}
case "payment_intent.payment_failed": {
const intent = event.data.object as Stripe.PaymentIntent;
await updateOrderStatus(intent.metadata.order_id, "payment_failed");
break;
}
case "account.updated": {
// ベンダーのアカウント状態変更(KYC完了・停止等)
const account = event.data.object as Stripe.Account;
await syncVendorAccountStatus(account.id, {
chargesEnabled: account.charges_enabled,
payoutsEnabled: account.payouts_enabled,
});
break;
}
case "transfer.created": {
// ベンダーへの送金完了
const transfer = event.data.object as Stripe.Transfer;
await logTransfer(transfer.id, transfer.amount);
break;
}
}
return NextResponse.json({ received: true });
}
6. ダッシュボード — ベンダーの収益管理
売上レポートの取得
// app/api/vendor/earnings/route.ts
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const accountId = searchParams.get("accountId");
const period = searchParams.get("period") || "30"; // 日数
const since = Math.floor(
Date.now() / 1000 - parseInt(period) * 24 * 60 * 60
);
// 指定期間のチャージを取得
const charges = await stripe.charges.list(
{ created: { gte: since }, limit: 100 },
{ stripeAccount: accountId } // ← Connect アカウント指定
);
// 残高確認
const balance = await stripe.balance.retrieve(
{},
{ stripeAccount: accountId }
);
const totalEarned = charges.data
.filter(c => c.status === "succeeded")
.reduce((sum, c) => sum + c.amount, 0);
const availableBalance = balance.available
.find(b => b.currency === "jpy")?.amount || 0;
return NextResponse.json({
totalEarned,
availableBalance,
pendingBalance: balance.pending
.find(b => b.currency === "jpy")?.amount || 0,
chargeCount: charges.data.length,
recentCharges: charges.data.slice(0, 10).map(c => ({
id: c.id,
amount: c.amount,
status: c.status,
created: c.created,
description: c.description,
})),
});
}
7. プラットフォーム管理 — 手数料収益の確認
// app/api/platform/revenue/route.ts
export async function GET(req: NextRequest) {
// プラットフォームの application fees を集計
const fees = await stripe.applicationFees.list({ limit: 100 });
const totalRevenue = fees.data
.filter(f => f.amount_refunded === 0)
.reduce((sum, f) => sum + f.amount, 0);
return NextResponse.json({
totalRevenue,
transactionCount: fees.data.length,
averageFee: fees.data.length > 0
? Math.floor(totalRevenue / fees.data.length)
: 0,
recentFees: fees.data.slice(0, 10),
});
}
8. Antigravity を活用した開発加速
エラーハンドリングの自動生成
Stripe APIのエラーは種類が多く、それぞれ適切な処理が必要です。Antigravityに以下を指示することで、完全なエラーハンドリングを自動生成できます。
Stripe エラーを網羅的にハンドリングする関数を生成してください。
card_declined / insufficient_funds / expired_card /
incorrect_cvc / processing_error / rate_limit に対して
それぞれ適切なユーザーメッセージを返す実装にしてください。
TypeScript + Stripe SDK のエラー型を正確に使用すること。
テストカードを使った Webhook テストの自動化
Stripe CLI を使った Webhook のローカルテスト手順と、
payment_intent.succeeded / payment_intent.payment_failed
の各イベントをシミュレートするテストスクリプトを生成してください。
個人開発者の視点から(実体験メモ)
ここまでの要点
Stripe Connect を使ったマーケットプレイス構築のポイントをまとめます。
- Express アカウント はマーケットプレイスの標準的な選択肢。KYCをStripeに任せられる
- Destination Charge で売上分配・手数料徴収を一括処理。資金移動業ライセンス不要
- Webhook で非同期な決済状態を確実に捕捉し、注文管理と連携する
- Antigravity でエラーハンドリング・テストコード・型定義を自動生成し、開発速度を上げる
Antigravity のサブスクリプション収益最適化ガイドも合わせてご覧ください。