subscription-integration
Dodo Paymentsのサブスクリプション統合を指導し、試用期間やプラン変更を含む定期課金機能を実装します。
npx skills add dodopayments/skills --skill subscription-integrationBefore / After 効果比較
1 组サブスクリプション決済連携の手動処理は、複雑でエラーが発生しやすく、試用期間やプラン変更などの循環課金機能の実装が困難であり、事業拡大とユーザーエクスペリエンスに影響を与えます。
Dodo Paymentsのサブスクリプション連携を指導し、試用期間やプラン変更を含む循環課金機能を実装することで、決済連携の効率とビジネスの柔軟性を大幅に向上させました。
description SKILL.md
subscription-integration
Dodo Payments Subscription Integration
Reference: docs.dodopayments.com/developer-resources/subscription-integration-guide
Implement recurring billing with trials, plan changes, and usage-based pricing.
Quick Start
1. Create Subscription Product
In the dashboard (Products → Create Product):
-
Select "Subscription" type
-
Set billing interval (monthly, yearly, etc.)
-
Configure pricing
2. Create Checkout Session
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
});
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'prod_monthly_plan', quantity: 1 }
],
subscription_data: {
trial_period_days: 14, // Optional trial
},
customer: {
email: 'subscriber@example.com',
name: 'Jane Doe',
},
return_url: 'https://yoursite.com/success',
});
// Redirect to session.checkout_url
3. Handle Webhook Events
// subscription.active - Grant access
// subscription.cancelled - Schedule access revocation
// subscription.renewed - Log renewal
// payment.succeeded - Track payments
Subscription Lifecycle
┌─────────────┐ ┌─────────┐ ┌────────┐
│ Created │ ──▶ │ Trial │ ──▶ │ Active │
└─────────────┘ └─────────┘ └────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌───────────┐
│ On Hold │ │ Cancelled │ │ Renewed │
└──────────┘ └───────────┘ └───────────┘
│ │
▼ ▼
┌──────────┐ ┌───────────┐
│ Failed │ │ Expired │
└──────────┘ └───────────┘
Webhook Events
Event When Action
subscription.active
Subscription starts
Grant access
subscription.updated
Any field changes
Sync state
subscription.on_hold
Payment fails
Notify user, retry
subscription.renewed
Successful renewal
Log, send receipt
subscription.plan_changed
Upgrade/downgrade
Update entitlements
subscription.cancelled
User cancels
Schedule end of access
subscription.failed
Mandate creation fails
Notify, retry options
subscription.expired
Term ends
Revoke access
Implementation Examples
Full Subscription Handler
// app/api/webhooks/subscription/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(req: NextRequest) {
const event = await req.json();
const data = event.data;
switch (event.type) {
case 'subscription.active':
await handleSubscriptionActive(data);
break;
case 'subscription.cancelled':
await handleSubscriptionCancelled(data);
break;
case 'subscription.on_hold':
await handleSubscriptionOnHold(data);
break;
case 'subscription.renewed':
await handleSubscriptionRenewed(data);
break;
case 'subscription.plan_changed':
await handlePlanChanged(data);
break;
case 'subscription.expired':
await handleSubscriptionExpired(data);
break;
}
return NextResponse.json({ received: true });
}
async function handleSubscriptionActive(data: any) {
const {
subscription_id,
customer,
product_id,
next_billing_date,
recurring_pre_tax_amount,
payment_frequency_interval,
} = data;
// Create or update user subscription
await prisma.subscription.upsert({
where: { externalId: subscription_id },
create: {
externalId: subscription_id,
userId: customer.customer_id,
email: customer.email,
productId: product_id,
status: 'active',
currentPeriodEnd: new Date(next_billing_date),
amount: recurring_pre_tax_amount,
interval: payment_frequency_interval,
},
update: {
status: 'active',
currentPeriodEnd: new Date(next_billing_date),
},
});
// Grant access
await prisma.user.update({
where: { id: customer.customer_id },
data: {
subscriptionStatus: 'active',
plan: product_id,
},
});
// Send welcome email
await sendWelcomeEmail(customer.email, product_id);
}
async function handleSubscriptionCancelled(data: any) {
const { subscription_id, customer, cancelled_at, cancel_at_next_billing_date } = data;
await prisma.subscription.update({
where: { externalId: subscription_id },
data: {
status: 'cancelled',
cancelledAt: new Date(cancelled_at),
// Keep access until end of billing period if cancel_at_next_billing_date
accessEndsAt: cancel_at_next_billing_date
? new Date(data.next_billing_date)
: new Date(),
},
});
// Send cancellation email
await sendCancellationEmail(customer.email, cancel_at_next_billing_date);
}
async function handleSubscriptionOnHold(data: any) {
const { subscription_id, customer } = data;
await prisma.subscription.update({
where: { externalId: subscription_id },
data: { status: 'on_hold' },
});
// Notify user about payment issue
await sendPaymentFailedEmail(customer.email);
}
async function handleSubscriptionRenewed(data: any) {
const { subscription_id, next_billing_date } = data;
await prisma.subscription.update({
where: { externalId: subscription_id },
data: {
status: 'active',
currentPeriodEnd: new Date(next_billing_date),
},
});
}
async function handlePlanChanged(data: any) {
const { subscription_id, product_id, recurring_pre_tax_amount } = data;
await prisma.subscription.update({
where: { externalId: subscription_id },
data: {
productId: product_id,
amount: recurring_pre_tax_amount,
},
});
// Update user entitlements based on new plan
await updateUserEntitlements(subscription_id, product_id);
}
async function handleSubscriptionExpired(data: any) {
const { subscription_id, customer } = data;
await prisma.subscription.update({
where: { externalId: subscription_id },
data: { status: 'expired' },
});
// Revoke access
await prisma.user.update({
where: { id: customer.customer_id },
data: {
subscriptionStatus: 'expired',
plan: null,
},
});
}
Subscription with Trial
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'prod_pro_monthly', quantity: 1 }
],
subscription_data: {
trial_period_days: 14,
},
customer: {
email: 'user@example.com',
name: 'John Doe',
},
return_url: 'https://yoursite.com/welcome',
});
Customer Portal for Self-Service
Allow customers to manage their subscription:
// Create portal session
const portal = await client.customers.createPortalSession({
customer_id: 'cust_xxxxx',
return_url: 'https://yoursite.com/account',
});
// Redirect to portal.url
Portal features:
-
View subscription details
-
Update payment method
-
Cancel subscription
-
View billing history
On-Demand (Usage-Based) Subscriptions
For metered/usage-based billing:
Create Subscription with Mandate
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'prod_usage_based', quantity: 1 }
],
customer: { email: 'user@example.com' },
return_url: 'https://yoursite.com/success',
});
Charge for Usage
// When usage occurs, create a charge
const charge = await client.subscriptions.charge({
subscription_id: 'sub_xxxxx',
amount: 1500, // $15.00 in cents
description: 'API calls for January 2025',
});
Track Usage Events
// payment.succeeded - Charge succeeded
// payment.failed - Charge failed, implement retry logic
Subscriptions with Credit Entitlements
Attach credit entitlements to subscription products to grant credits each billing cycle:
Setup
-
Create a credit entitlement (Dashboard → Products → Credits)
-
Create/edit a subscription product
-
In Entitlements section, click Attach next to Credits
-
Configure: credits per cycle, trial credits, proration, low balance threshold
Checkout with Credits
// Product has credit entitlement attached (e.g., 10,000 AI tokens/month)
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'prod_pro_with_credits', quantity: 1 }
],
subscription_data: {
trial_period_days: 14, // Trial credits can differ from regular amount
},
customer: { email: 'user@example.com' },
return_url: 'https://yoursite.com/success',
});
Credit Lifecycle per Cycle
Each billing cycle:
-
New credits issued —
credit.addedwebhook fires -
Usage deducts credits — Automatically via meters or manually via API
-
Cycle ends — Unused credits expire or roll over based on settings
-
Overage handled — Forgiven, billed, or carried as deficit
Handle Credit Webhooks in Subscription Context
case 'credit.added':
// Credits issued with subscription renewal
await syncCreditBalance(data.customer_id, data.credit_entitlement_id, data.balance_after);
break;
case 'credit.balance_low':
// Notify customer or suggest upgrade
await sendLowBalanceAlert(data.customer_id, data.credit_entitlement_name, data.available_balance);
break;
case 'credit.deducted':
// Track consumption for analytics
await logCreditUsage(data.customer_id, data.amount);
break;
Plan Changes with Credits
When customers upgrade/downgrade, credit proration can be enabled:
-
Proration enabled: Remaining credits are prorated based on time left in cycle
-
Proration disabled: Credits continue as-is until next cycle
Plan Changes
Upgrade/Downgrade Flow
// Get available plans
const plans = await client.products.list({
type: 'subscription',
});
// Change plan
await client.subscriptions.update({
subscription_id: 'sub_xxxxx',
product_id: 'prod_new_plan',
proration_behavior: 'create_prorations', // or 'none'
});
Handling subscription.plan_changed
async function handlePlanChanged(data: any) {
const { subscription_id, product_id, customer } = data;
// Map product to features/limits
const planFeatures = getPlanFeatures(product_id);
await prisma.user.update({
where: { externalId: customer.customer_id },
data: {
plan: product_id,
features: planFeatures,
apiLimit: planFeatures.apiLimit,
storageLimit: planFeatures.storageLimit,
},
});
}
Access Control Pattern
Middleware Example (Next.js)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
// Check subscription status
const session = await getSession(request);
if (!session?.user) {
return NextResponse.redirect(new URL('/login', request.url));
}
const subscription = await getSubscription(session.user.id);
// Check if accessing premium feature
if (request.nextUrl.pathname.startsWith('/dashboard/pro')) {
if (!subscription || subscription.status !== 'active') {
return NextResponse.redirect(new URL('/pricing', request.url));
}
// Check if plan includes this feature
if (!subscription.features.includes('pro')) {
return NextResponse.redirect(new URL('/upgrade', request.url));
}
}
return NextResponse.next();
}
React Hook for Subscription State
// hooks/useSubscription.ts
import useSWR from 'swr';
export function useSubscription() {
const { data, error, mutate } = useSWR('/api/subscription', fetcher);
return {
subscription: data,
isLoading: !error && !data,
isError: error,
isActive: data?.status === 'active',
isPro: data?.plan?.includes('pro'),
refresh: mutate,
};
}
// Usage in component
function PremiumFeature() {
const { isActive, isPro } = useSubscription();
if (!isActive) {
return <UpgradePrompt />;
}
if (!isPro) {
return <ProUpgradePrompt />;
}
return <ActualFeature />;
}
Common Patterns
Grace Period for Failed Payments
async function handleSubscriptionOnHold(data: any) {
const gracePeriodDays = 7;
await prisma.subscription.update({
where: { externalId: data.subscription_id },
data: {
status: 'on_hold',
gracePeriodEnds: new Date(Date.now() + gracePeriodDays * 24 * 60 * 60 * 1000),
},
});
// Schedule job to revoke access after grace period
await scheduleAccessRevocation(data.subscription_id, gracePeriodDays);
}
Prorated Upgrades
When upgrading mid-cycle:
// Dodo handles proration automatically
// Customer pays difference for remaining days
await client.subscriptions.update({
subscription_id: 'sub_xxxxx',
product_id: 'prod_higher_plan',
proration_behavior: 'create_prorations',
});
Cancellation with End-of-Period Access
// subscription.cancelled event includes:
// - cancel_at_next_billing_date: boolean
// - next_billing_date: string (when access should end)
if (data.cancel_at_next_billing_date) {
// Keep access until next_billing_date
await scheduleAccessRevocation(
data.subscription_id,
new Date(data.next_billing_date)
);
}
Testing
Test Scenarios
-
New subscription →
subscription.active -
Renewal success →
subscription.renewed+payment.succeeded -
Renewal failure →
subscription.on_hold+payment.failed -
Plan upgrade →
subscription.plan_changed -
Cancellation →
subscription.cancelled -
Expiration →
subscription.expired
Test in Dashboard
Use test mode and trigger events manually from the webhook settings.
Resources
Weekly Installs194Repositorydodopayments/skillsGitHub Stars7First SeenJan 21, 2026Security AuditsGen Agent Trust HubPassSocketPassSnykWarnInstalled onopencode161gemini-cli159codex150github-copilot144cursor129kimi-cli124
forumユーザーレビュー (0)
レビューを書く
レビューなし
統計データ
ユーザー評価
この Skill を評価