Dodo Payments Subscription Integration
SkillCommerce & financeGuide for managing recurring subscriptions after checkout, including trials, lifecycle states, plan changes, cancellation, failed-payment recovery, proration, mandates, and on-demand charges.
Use Dodo Payments Subscription Integration in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Dodo Payments Subscription Integration and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Dodo Payments Subscription Integration skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by hashgraph-online/awesome-codex-plugins in plugins/dodopayments/dodo-agent-plugin/skills/subscription-integration/SKILL.md and read by Ahel’s review.
Implement recurring billing with trials, plan changes, and on-demand charging. Subscriptions are created through Checkout Sessions, managed via the subscriptions API, and monitored through webhooks.
When to use this skill
- Building a subscription product with recurring billing and trials
- Handling plan upgrades, downgrades, and migrations mid-cycle
- Implementing on-demand/off-session charging with mandates
- Managing failed payments and dunning recovery
- Building a customer self-service portal for subscription management
Core Concepts
Subscription lifecycle: The six subscription statuses are pending, active, on_hold, cancelled, failed, and expired. A trialing subscription reports active; subscription.renewed is an event, not a status. Failed payments can move a subscription to on_hold (recoverable) or failed (terminal). Cancellation sets it to cancelled or schedules it to become expired at period end.
Checkout Sessions: The recommended path for creating subscriptions. A single-use hosted checkout that collects payment and customer data, then creates the subscription server-side.
Proration: When a customer changes plans mid-cycle, Dodo calculates credits or charges based on the time remaining. Proration mode controls whether the customer is billed immediately, credited, or neither.
Mandates: Authorization to charge a customer's payment method repeatedly (for subscriptions) or on-demand (for usage-based billing). Created during Checkout, can be updated if payment fails.
Creating a Subscription
Via Checkout Session (Recommended)
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_monthly_plan', quantity: 1 }
],
subscription_data: {
trial_period_days: 14, // Optional
},
customer: {
email: 'subscriber@example.com',
name: 'Jane Doe',
},
return_url: 'https://yoursite.com/success',
});
// Redirect user to session.checkout_url
console.log('Redirect to:', session.checkout_url);
The customer completes payment on the hosted checkout. On success, Dodo creates the subscription and fires the subscription.active webhook.
With Add-ons
const session = await client.checkoutSessions.create({
product_cart: [
{
product_id: 'pdt_pro_monthly',
quantity: 1,
addons: [
{ addon_id: 'adn_extra_seats', quantity: 3 }
]
}
],
subscription_data: {
trial_period_days: 7,
},
customer: { email: 'user@example.com' },
return_url: 'https://yoursite.com/success',
});
Subscription Lifecycle & Status
| Status | Meaning | Transitions |
|---|---|---|
pending | Creation in progress | → active, failed |
active | Actively renewing | → on_hold, cancelled, expired |
on_hold | Renewal/plan-change payment failed; recoverable | → active (payment method updated), failed (retries exhausted), cancelled |
cancelled | Will not renew | → expired (at period end) |
failed | Initial mandate/payment failed; terminal | (no recovery) |
expired | Subscription term ended | (terminal) |
Trial period: If trial_period_days is set, the subscription enters active immediately but charges nothing until the trial ends. The first charge occurs on the trial end date.
Subscription Methods
Retrieve
const subscription = await client.subscriptions.retrieve('sub_xxxxx');
console.log(subscription.status, subscription.next_billing_date);
Update Payment Method
When a subscription is on_hold due to failed payment, the customer can update their payment method. This automatically charges any outstanding dues.
await client.subscriptions.updatePaymentMethod('sub_xxxxx', {
payment_method: {
type: 'existing',
payment_method_id: 'pm_new_method',
},
});
Success emits payment.succeeded followed by subscription.active.
List Usage History (Metered Subscriptions)
const history = await client.subscriptions.retrieveUsageHistory('sub_xxxxx', {
page_size: 50,
page_number: 0,
});
Retrieve Credit Usage
const creditUsage = await client.subscriptions.retrieveCreditUsage('sub_xxxxx');
console.log('Subscription:', creditUsage.subscription_id);
for (const item of creditUsage.items) {
console.log(item.credit_entitlement_name, item.balance); // balance is a string
}
Plan Changes (Upgrades & Downgrades)
Change Plan
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_higher_tier',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
on_payment_failure: 'prevent_change',
});
Proration modes:
| Mode | Upgrade | Downgrade | Billing date |
|---|---|---|---|
prorated_immediately | Time-prorated charge | Time-prorated credit | Resets to change date |
difference_immediately | Full new-plan charge | Difference becomes credit | Resets |
full_immediately | Full new-plan charge | Full new-plan charge, no credit | Resets |
do_not_bill | No charge | No credit | Preserved |
Payment failure handling:
prevent_change: Keep the old plan if the charge fails.apply_change: Apply the new plan even if payment fails (subscription may becomeon_hold).
Preview Plan Change
Show the customer a quote before committing:
const preview = await client.subscriptions.previewChangePlan('sub_xxxxx', {
product_id: 'pdt_new_plan',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
});
console.log('Effective at:', preview.immediate_charge.effective_at);
console.log('Line items:', preview.immediate_charge.line_items);
console.log('Summary:', preview.immediate_charge.summary);
console.log('New plan:', preview.new_plan);
Cancel a Scheduled Plan Change
await client.subscriptions.cancelChangePlan('sub_xxxxx');
Cancellation
Immediate Cancellation
await client.subscriptions.update('sub_xxxxx', {
status: 'cancelled',
});
Access is revoked immediately.
Cancel at Period End
await client.subscriptions.update('sub_xxxxx', {
cancel_at_next_billing_date: true,
});
The subscription remains active until the next billing date, then transitions to expired. The subscription.cancelled webhook includes cancel_at_next_billing_date: true and next_billing_date so you know when to revoke access.
On-Demand (Off-Session) Charging
For usage-based or metered subscriptions, charge the customer on-demand without a scheduled renewal.
Create Subscription with Mandate
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_usage_based', quantity: 1 }
],
subscription_data: {
on_demand: {
mandate_only: true,
}
},
customer: { email: 'user@example.com' },
return_url: 'https://yoursite.com/success',
});
This creates a subscription with a mandate but no automatic renewal. You control when to charge.
Charge On-Demand
const charge = await client.subscriptions.charge('sub_xxxxx', {
product_price: 2500, // $25.00 in cents
product_currency: 'USD',
product_description: 'API calls for January 2025',
});
console.log('Payment ID:', charge.payment_id);
Amounts are in the smallest currency unit (cents for USD, paise for INR, etc.).
Dodo does not automatically retry on-demand charges. You own retry logic and decline filtering. Monitor payment.failed webhooks and implement your own retry strategy.
Failed Payments & Recovery
On-Hold Subscriptions
When a renewal or plan-change charge fails, the subscription moves to on_hold. This is recoverable.
// Listen for subscription.on_hold webhook
case 'subscription.on_hold':
// Notify customer, offer payment method update
await sendPaymentFailedEmail(data.customer.customer_id);
break;
Recovery Options
-
Payment Retries: Opt-in under Settings → Recovery. Dodo retries failed renewals up to 8 times over a configurable 1–30 day window (default 13 days). Only for soft declines.
-
Customer Portal: Customer updates payment method → automatic charge for outstanding dues →
subscription.active. -
Manual Charge: You update the payment method server-side, then call
updatePaymentMethod().
Dunning
Dunning sends up to four configurable emails for on_hold renewals and customer-portal cancellations. Exhausted dunning does not change the subscription state; you must handle the final outcome.
Customer Portal
Allow customers to self-serve: view subscriptions, update payment methods, cancel, and upgrade/downgrade.
const portalSession = await client.customers.customerPortal.create(
'cus_xxxxx',
{ return_url: 'https://yoursite.com/account' }
);
// Redirect to portalSession.link
Portal links expire after 24 hours. Customers can:
- View subscription details and renewal dates
- Cancel immediately or at period end
- Upgrade/downgrade within enabled Product Collections
- Update payment methods and reactivate
on_holdsubscriptions - View billing history and download invoices
For full customer management (creating, updating, listing), see the customer-management skill.
Webhook Events
| Event | When | Action |
|---|---|---|
subscription.active | Subscription becomes active, including a trial start or recovery | Grant access |
subscription.renewed | Successful renewal | Log renewal, send receipt |
subscription.on_hold | Renewal/plan-change payment failed | Notify customer, offer recovery |
subscription.plan_changed | Plan upgraded/downgraded or add-ons changed | Update entitlements |
subscription.cancelled | Customer cancels | Schedule access revocation per cancel_at_next_billing_date |
subscription.failed | Initial mandate/payment failed | Notify customer, offer retry or new subscription |
subscription.expired | Subscription term ended | Revoke access |
Webhook signature verification, raw-body handling, durable processing, and idempotency are covered in the webhook-integration skill.
Example Handler
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const raw = await req.text();
const headers = Object.fromEntries(req.headers.entries());
const event = await client.webhooks.unwrap(raw, { headers });
const webhookId = req.headers.get('webhook-id');
if (!webhookId) {
return NextResponse.json({ error: 'Missing webhook-id' }, { status: 400 });
}
// Implement this as an atomic insert backed by a UNIQUE constraint.
// Keep the claim and entitlement changes in the same database transaction.
const claimed = await claimWebhookId(webhookId);
if (!claimed) {
return NextResponse.json({ received: true });
}
switch (event.type) {
case 'subscription.active':
await grantAccess(event.data.customer.customer_id, event.data.product_id);
break;
case 'subscription.on_hold':
await notifyPaymentFailed(event.data.customer.customer_id);
break;
case 'subscription.cancelled':
if (event.data.cancel_at_next_billing_date) {
await scheduleAccessRevocation(event.data.subscription_id, new Date(event.data.next_billing_date));
} else {
await revokeAccessImmediately(event.data.subscription_id);
}
break;
case 'subscription.expired':
await revokeAccess(event.data.customer.customer_id);
break;
}
return NextResponse.json({ received: true });
}
Credit Entitlements
Attach credit entitlements to subscription products to grant credits each billing cycle. For full credit management, see the credit-based-billing skill.
Quick example:
// Product has credit entitlement attached (e.g., 10,000 tokens/month)
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_pro_with_credits', quantity: 1 }
],
subscription_data: {
trial_period_days: 14,
},
customer: { email: 'user@example.com' },
return_url: 'https://yoursite.com/success',
});
On each renewal, credits are issued. Monitor credit.added and credit.deducted webhooks to sync your ledger.
Common Mistakes
1. Granting Access on return_url Instead of Webhook
Wrong:
// On return_url redirect
await grantAccess(user.id);
The return_url is hit before the subscription is fully created. Dodo may still be processing the mandate or payment. Always wait for subscription.active webhook.
Right:
// In webhook handler
case 'subscription.active':
await grantAccess(data.customer.customer_id);
break;
2. Using Deprecated subscriptions.create
Wrong:
const sub = await client.subscriptions.create({
product_id: 'pdt_monthly',
customer_id: 'cus_xxxxx',
});
This endpoint is deprecated. Use Checkout Sessions.
Right:
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_monthly', quantity: 1 }],
customer: { customer_id: 'cus_xxxxx' },
return_url: 'https://yoursite.com/success',
});
3. Misinterpreting subscription.plan_changed
subscription.plan_changed fires for upgrades, downgrades, add-on changes, AND when cancel_at_next_billing_date is toggled. Don't assume every plan_changed event means a paid upgrade succeeded. Inspect the payload and check the subscription's current product_id.
4. Forgetting Proration Mode
Wrong:
// Missing proration_billing_mode
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_new',
quantity: 1,
// proration_billing_mode: 'prorated_immediately', // REQUIRED
});
This will fail. Always specify a proration mode.
5. Confusing on_hold with Pause
on_hold means payment failed, not that the customer paused. There is no general pause/resume operation. on_hold is recoverable only by updating the payment method or waiting for retry.
6. Not Handling cancel_at_next_billing_date
// Wrong: revoke immediately
await revokeAccess(data.customer.customer_id);
// Right: check the flag
if (data.cancel_at_next_billing_date) {
await scheduleAccessRevocation(data.subscription_id, new Date(data.next_billing_date));
} else {
await revokeAccessImmediately(data.subscription_id);
}
Resources
Signals
- GitHub stars
- 1k
- Forks
- 316
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
subscription-integration- Source
- github.com/hashgraph-online/awesome-codex-plugins
github.com/hashgraph-online/awesome-codex-plugins
Related picks
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScriptchurn-prevention
Skill · coreyhaines31
More in Commerce & financeagent-payment-x402
Skill · affaan-m
More in Commerce & financecustomer-billing-ops
Skill · affaan-m
More in Commerce & financereview
Skill · thedaviddias
More in Commerce & finance