subscription-integration

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

INSTALLATION
npx skills add https://github.com/dodopayments/skills --skill subscription-integration
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card