Home/财务与会计/subscription-integration
S

subscription-integration

by @dodopaymentsv
4.8(5)

Guides Dodo Payments subscription integration, implementing recurring billing features including trials and plan changes.

Subscription ManagementRecurring BillingPayment GatewaysStripe SubscriptionsSaaS Business ModelsGitHub
Installation
npx skills add dodopayments/skills --skill subscription-integration
compare_arrows

Before / After Comparison

1
Before

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.

After

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

Weekly Installs194Repositorydodopayments/skillsGitHub Stars7First SeenJan 21, 2026Security AuditsGen Agent Trust HubPassSocketPassSnykWarnInstalled onopencode161gemini-cli159codex150github-copilot144cursor129kimi-cli124

forumUser Reviews (0)

Write a Review

Effect
Usability
Docs
Compatibility

No reviews yet

Statistics

Installs209
Rating4.8 / 5.0
Version
Updated2026年3月17日
Comparisons1

User Rating

4.8(5)
5
0%
4
0%
3
0%
2
0%
1
0%

Rate this Skill

0.0

Compatible Platforms

🔧Claude Code
🔧OpenClaw
🔧OpenCode
🔧Codex
🔧Gemini CLI
🔧GitHub Copilot
🔧Amp
🔧Kimi CLI

Timeline

Created2026年3月17日
Last Updated2026年3月17日