subscription-integration

subscription-integration

Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.

5étoiles
0forks
Mis à jour 1/22/2026
SKILL.md
readonlyread-only
name
subscription-integration
description

Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.

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

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

  1. New subscription → subscription.active
  2. Renewal success → subscription.renewed + payment.succeeded
  3. Renewal failure → subscription.on_hold + payment.failed
  4. Plan upgrade → subscription.plan_changed
  5. Cancellation → subscription.cancelled
  6. Expiration → subscription.expired

Test in Dashboard

Use test mode and trigger events manually from the webhook settings.


Resources

You Might Also Like

Related Skills

gog

gog

169Kdev-api

Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.

openclaw avataropenclaw
Obtenir
weather

weather

169Kdev-api

Get current weather and forecasts (no API key required).

openclaw avataropenclaw
Obtenir

Guide for implementing oRPC contract-first API patterns in Dify frontend. Triggers when creating new API contracts, adding service endpoints, integrating TanStack Query with typed contracts, or migrating legacy service calls to oRPC. Use for all API layer work in web/contract and web/service directories.

langgenius avatarlanggenius
Obtenir
blucli

blucli

92Kdev-api

BluOS CLI (blu) for discovery, playback, grouping, and volume.

moltbot avatarmoltbot
Obtenir
ordercli

ordercli

92Kdev-api

Foodora-only CLI for checking past orders and active order status (Deliveroo WIP).

moltbot avatarmoltbot
Obtenir
gifgrep

gifgrep

92Kdev-api

Search GIF providers with CLI/TUI, download results, and extract stills/sheets.

moltbot avatarmoltbot
Obtenir