subscription-integration

Dodo Payments Subscription Integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "subscription-integration" with this command: npx skills add dodopayments/skills/dodopayments-skills-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

  1. 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

  1. 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.added webhook 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

  • Subscription Guide

  • On-Demand Subscriptions

  • Webhook Events

  • Customer Portal

  • Credit-Based Billing

  • Credit Webhook Events

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

webhook-integration

No summary provided by upstream source.

Repository SourceNeeds Review
General

dodo-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

checkout-integration

No summary provided by upstream source.

Repository SourceNeeds Review