.agent/skills/subscription-integration/SKILL.md
Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.
npx skillsauth add akghosh111/scyra subscription-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference: docs.dodopayments.com/developer-resources/subscription-integration-guide
Implement recurring billing with trials, plan changes, and usage-based pricing.
In the dashboard (Products → Create Product):
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: '[email protected]',
name: 'Jane Doe',
},
return_url: 'https://yoursite.com/success',
});
// Redirect to session.checkout_url
// subscription.active - Grant access
// subscription.cancelled - Schedule access revocation
// subscription.renewed - Log renewal
// payment.succeeded - Track payments
┌─────────────┐ ┌─────────┐ ┌────────┐
│ Created │ ──▶ │ Trial │ ──▶ │ Active │
└─────────────┘ └─────────┘ └────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌───────────┐
│ On Hold │ │ Cancelled │ │ Renewed │
└──────────┘ └───────────┘ └───────────┘
│ │
▼ ▼
┌──────────┐ ┌───────────┐
│ Failed │ │ Expired │
└──────────┘ └───────────┘
| 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 |
// 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,
},
});
}
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'prod_pro_monthly', quantity: 1 }
],
subscription_data: {
trial_period_days: 14,
},
customer: {
email: '[email protected]',
name: 'John Doe',
},
return_url: 'https://yoursite.com/welcome',
});
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:
For metered/usage-based billing:
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'prod_usage_based', quantity: 1 }
],
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
// 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',
});
// payment.succeeded - Charge succeeded
// payment.failed - Charge failed, implement retry logic
// 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'
});
subscription.plan_changedasync 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,
},
});
}
// 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();
}
// 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 />;
}
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);
}
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',
});
// 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)
);
}
subscription.activesubscription.renewed + payment.succeededsubscription.on_hold + payment.failedsubscription.plan_changedsubscription.cancelledsubscription.expiredUse test mode and trigger events manually from the webhook settings.
development
Complete guide for setting up and handling Dodo Payments webhooks for real-time payment event notifications.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
tools
This skill provides guidance and enforcement rules for implementing secure two-factor authentication (2FA) using Better Auth's twoFactor plugin.
documentation
Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.