subscription-integration
Guides Dodo Payments subscription integration, implementing recurring billing features including trials and plan changes.
npx skills add dodopayments/skills --skill subscription-integrationBefore / After Comparison
1 组Manually handling subscription payment integration is complex and error-prone, making it difficult to implement recurring billing features such as trials and plan changes, which impacts business expansion and user experience.
Guided Dodo Payments subscription integration to implement recurring billing features, including trials and plan changes, significantly improving payment integration efficiency and business flexibility.
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
forumUser Reviews (0)
Write a Review
No reviews yet
Statistics
User Rating
Rate this Skill