---
id: sm-subscription-integration
name: "subscription-integration"
url: https://skills.yangsir.net/skill/sm-subscription-integration
author: dodopayments
domain: finance
tags: ["subscription-management", "recurring-billing", "payment-gateways", "stripe-subscriptions", "saas-business-models"]
install_count: 519
rating: 4.20 (20 reviews)
github: https://github.com/dodopayments/skills
---

# subscription-integration

> 指导Dodo Payments订阅集成，实现包含试用和计划变更的循环计费功能。

**Stats**: 519 installs · 4.2/5 (20 reviews)

## Before / After 对比

### 订阅支付集成效率对比

## Readme

# subscription-integration

# Dodo Payments Subscription Integration

**Reference: [docs.dodopayments.com/developer-resources/subscription-integration-guide](https://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.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](https://docs.dodopayments.com/developer-resources/subscription-integration-guide)

- [On-Demand Subscriptions](https://docs.dodopayments.com/developer-resources/ondemand-subscriptions)

- [Webhook Events](https://docs.dodopayments.com/developer-resources/webhooks/intents/subscription)

- [Customer Portal](https://docs.dodopayments.com/developer-resources/customer-portal)

- [Credit-Based Billing](https://docs.dodopayments.com/features/credit-based-billing)

- [Credit Webhook Events](https://docs.dodopayments.com/developer-resources/webhooks/intents/credit)

Weekly Installs194Repository[dodopayments/skills](https://github.com/dodopayments/skills)GitHub Stars7First SeenJan 21, 2026Security Audits[Gen Agent Trust HubPass](/dodopayments/skills/subscription-integration/security/agent-trust-hub)[SocketPass](/dodopayments/skills/subscription-integration/security/socket)[SnykWarn](/dodopayments/skills/subscription-integration/security/snyk)Installed onopencode161gemini-cli159codex150github-copilot144cursor129kimi-cli124

---
*Source: https://skills.yangsir.net/skill/sm-subscription-integration*
*Markdown mirror: https://skills.yangsir.net/api/skill/sm-subscription-integration/markdown*