Skip to main content
Upsells and downsells let you offer additional products or plan changes to customers using their saved payment methods. This enables one-click purchases that skip payment collection, dramatically improving conversion rates.

Post-Purchase Upsells

Offer complementary products immediately after checkout with one-click purchasing.

Subscription Upgrades

Move customers to higher tiers with automatic proration and instant billing.

Cross-Sells

Add related products to existing customers without re-entering payment details.

Overview

Upsells and downsells are powerful revenue optimization strategies:
  • Upsells: Offer a higher-value product or upgrade (e.g., Pro plan instead of Basic)
  • Downsells: Offer a lower-priced alternative when a customer declines or downgrades
  • Cross-sells: Suggest complementary products (e.g., add-ons, related items)
Dodo Payments enables these flows through the payment_method_id parameter, which lets you charge a customer’s saved payment method without requiring them to re-enter card details.

Key Benefits

BenefitImpact
One-click purchasesSkip payment form entirely for returning customers
Higher conversionReduce friction at the moment of decision
Instant processingCharges process immediately with confirm: true
Seamless UXCustomers stay in your app throughout the flow

How It Works

Prerequisites

Before implementing upsells and downsells, ensure you have:
1

Customer with Saved Payment Method

Customers must have completed at least one purchase. Payment methods are automatically saved when customers complete checkout.
2

Products Configured

Create your upsell products in the Dodo Payments dashboard. These can be one-time payments, subscriptions, or add-ons.
3

Webhook Endpoint

Set up webhooks to handle payment.succeeded, payment.failed, and subscription.plan_changed events.

Getting Customer Payment Methods

Before offering an upsell, retrieve the customer’s saved payment methods:
import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: 'live_mode',
});

async function getPaymentMethods(customerId: string) {
  const paymentMethods = await client.customers.listPaymentMethods(customerId);
  
  // Returns array of saved payment methods
  // Each has: payment_method_id, type, card (last4, brand, exp_month, exp_year)
  return paymentMethods;
}

// Example usage
const methods = await getPaymentMethods('cus_123');
console.log('Available payment methods:', methods);

// Use the first available method for upsell
const primaryMethod = methods[0]?.payment_method_id;
Payment methods are automatically saved when customers complete checkout. You don’t need to explicitly save them.

Post-Purchase One-Click Upsells

Offer additional products immediately after a successful purchase. The customer can accept with a single click since their payment method is already saved.

Implementation

import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: 'live_mode',
});

async function createOneClickUpsell(
  customerId: string,
  paymentMethodId: string,
  upsellProductId: string
) {
  // Create checkout session with saved payment method
  // confirm: true processes the payment immediately
  const session = await client.checkoutSessions.create({
    product_cart: [
      {
        product_id: upsellProductId,
        quantity: 1
      }
    ],
    customer: {
      customer_id: customerId
    },
    payment_method_id: paymentMethodId,
    confirm: true,  // Required when using payment_method_id
    return_url: 'https://yourapp.com/upsell-success',
    feature_flags: {
      redirect_immediately: true  // Skip success page
    },
    metadata: {
      upsell_source: 'post_purchase',
      original_order_id: 'order_123'
    }
  });

  return session;
}

// Example: Offer premium add-on after initial purchase
async function handlePostPurchaseUpsell(customerId: string) {
  // Get customer's payment methods
  const methods = await client.customers.listPaymentMethods(customerId);
  
  if (methods.length === 0) {
    console.log('No saved payment methods available');
    return null;
  }

  // Create the upsell with one-click checkout
  const upsell = await createOneClickUpsell(
    customerId,
    methods[0].payment_method_id,
    'prod_premium_addon'
  );

  console.log('Upsell processed:', upsell.session_id);
  return upsell;
}
When using payment_method_id, you must set confirm: true and provide an existing customer_id. The payment method must belong to that customer.

Subscription Upgrades

Move customers to higher-tier subscription plans with automatic proration handling.

Preview Before Committing

Always preview plan changes to show customers exactly what they’ll be charged:
async function previewUpgrade(
  subscriptionId: string,
  newProductId: string
) {
  const preview = await client.subscriptions.previewChangePlan(subscriptionId, {
    product_id: newProductId,
    quantity: 1,
    proration_billing_mode: 'difference_immediately'
  });

  return {
    immediateCharge: preview.immediate_charge?.summary,
    newPlan: preview.new_plan,
    effectiveDate: preview.effective_date
  };
}

// Show customer the charge before confirming
const preview = await previewUpgrade('sub_123', 'prod_pro_plan');
console.log(`Upgrade will charge: ${preview.immediateCharge}`);

Execute the Upgrade

async function upgradeSubscription(
  subscriptionId: string,
  newProductId: string,
  prorationMode: 'prorated_immediately' | 'difference_immediately' | 'full_immediately' = 'difference_immediately'
) {
  const result = await client.subscriptions.changePlan(subscriptionId, {
    product_id: newProductId,
    quantity: 1,
    proration_billing_mode: prorationMode
  });

  return {
    status: result.status,
    subscriptionId: result.subscription_id,
    paymentId: result.payment_id,
    invoiceId: result.invoice_id
  };
}

// Upgrade from Basic ($30) to Pro ($80)
// With difference_immediately: charges $50 instantly
const upgrade = await upgradeSubscription('sub_123', 'prod_pro_plan');
console.log('Upgrade status:', upgrade.status);

Proration Modes

Choose how customers are billed when upgrading:
ModeBehaviorBest For
difference_immediatelyCharges price difference instantly (3030→80 = $50)Simple upgrades
prorated_immediatelyCharges based on remaining time in billing cycleFair time-based billing
full_immediatelyCharges full new plan price, ignores remaining timeBilling cycle resets
Use difference_immediately for straightforward upgrade flows. Use prorated_immediately when you want to account for unused time on the current plan.

Cross-Sells

Add complementary products for existing customers without requiring them to re-enter payment details.

Implementation

async function createCrossSell(
  customerId: string,
  paymentMethodId: string,
  productId: string,
  quantity: number = 1
) {
  // Create a one-time payment using saved payment method
  const payment = await client.payments.create({
    product_cart: [
      {
        product_id: productId,
        quantity: quantity
      }
    ],
    customer_id: customerId,
    payment_method_id: paymentMethodId,
    return_url: 'https://yourapp.com/purchase-complete',
    metadata: {
      purchase_type: 'cross_sell',
      source: 'product_recommendation'
    }
  });

  return payment;
}

// Example: Customer bought a course, offer related ebook
async function offerRelatedProduct(customerId: string, relatedProductId: string) {
  const methods = await client.customers.listPaymentMethods(customerId);
  
  if (methods.length === 0) {
    // Fall back to standard checkout
    return client.checkoutSessions.create({
      product_cart: [{ product_id: relatedProductId, quantity: 1 }],
      customer: { customer_id: customerId },
      return_url: 'https://yourapp.com/purchase-complete'
    });
  }

  // One-click purchase
  return createCrossSell(customerId, methods[0].payment_method_id, relatedProductId);
}

Subscription Downgrades

When customers want to move to a lower-tier plan, handle the transition gracefully with automatic credits.

How Downgrades Work

  1. Customer requests downgrade (Pro → Basic)
  2. System calculates remaining value on current plan
  3. Credit is added to subscription for future renewals
  4. Customer moves to new plan immediately
async function downgradeSubscription(
  subscriptionId: string,
  newProductId: string
) {
  // Preview the downgrade first
  const preview = await client.subscriptions.previewChangePlan(subscriptionId, {
    product_id: newProductId,
    quantity: 1,
    proration_billing_mode: 'difference_immediately'
  });

  console.log('Credit to be applied:', preview.credit_amount);

  // Execute the downgrade
  const result = await client.subscriptions.changePlan(subscriptionId, {
    product_id: newProductId,
    quantity: 1,
    proration_billing_mode: 'difference_immediately'
  });

  // Credits are automatically applied to future renewals
  return result;
}

// Downgrade from Pro ($80) to Basic ($30)
// $50 credit added to subscription, auto-applied on next renewal
const downgrade = await downgradeSubscription('sub_123', 'prod_basic_plan');
Credits from downgrades using difference_immediately are subscription-scoped and automatically applied to future renewals. They’re distinct from Customer Credits.

Complete Example: Post-Purchase Upsell Flow

Here’s a complete implementation showing how to offer an upsell after a successful purchase:
import DodoPayments from 'dodopayments';
import express from 'express';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: 'live_mode',
});

const app = express();

// Store for tracking upsell eligibility (use your database in production)
const eligibleUpsells = new Map<string, { customerId: string; productId: string }>();

// Webhook handler for initial purchase success
app.post('/webhooks/dodo', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = JSON.parse(req.body.toString());
  
  switch (event.type) {
    case 'payment.succeeded':
      // Check if customer is eligible for upsell
      const customerId = event.data.customer_id;
      const productId = event.data.product_id;
      
      // Define upsell rules (e.g., bought Basic, offer Pro)
      const upsellProduct = getUpsellProduct(productId);
      
      if (upsellProduct) {
        eligibleUpsells.set(customerId, {
          customerId,
          productId: upsellProduct
        });
      }
      break;
      
    case 'payment.failed':
      console.log('Payment failed:', event.data.payment_id);
      // Handle failed upsell payment
      break;
  }
  
  res.json({ received: true });
});

// API endpoint to check upsell eligibility
app.get('/api/upsell/:customerId', async (req, res) => {
  const { customerId } = req.params;
  const upsell = eligibleUpsells.get(customerId);
  
  if (!upsell) {
    return res.json({ eligible: false });
  }
  
  // Get payment methods
  const methods = await client.customers.listPaymentMethods(customerId);
  
  if (methods.length === 0) {
    return res.json({ eligible: false, reason: 'no_payment_method' });
  }
  
  // Get product details for display
  const product = await client.products.retrieve(upsell.productId);
  
  res.json({
    eligible: true,
    product: {
      id: product.product_id,
      name: product.name,
      price: product.price,
      currency: product.currency
    },
    paymentMethodId: methods[0].payment_method_id
  });
});

// API endpoint to accept upsell
app.post('/api/upsell/:customerId/accept', async (req, res) => {
  const { customerId } = req.params;
  const upsell = eligibleUpsells.get(customerId);
  
  if (!upsell) {
    return res.status(400).json({ error: 'No upsell available' });
  }
  
  try {
    const methods = await client.customers.listPaymentMethods(customerId);
    
    // Create one-click purchase
    const session = await client.checkoutSessions.create({
      product_cart: [{ product_id: upsell.productId, quantity: 1 }],
      customer: { customer_id: customerId },
      payment_method_id: methods[0].payment_method_id,
      confirm: true,
      return_url: `${process.env.APP_URL}/upsell-success`,
      feature_flags: { redirect_immediately: true },
      metadata: { upsell: 'true', source: 'post_purchase' }
    });
    
    // Clear the upsell offer
    eligibleUpsells.delete(customerId);
    
    res.json({ success: true, sessionId: session.session_id });
  } catch (error) {
    console.error('Upsell failed:', error);
    res.status(500).json({ error: 'Upsell processing failed' });
  }
});

// Helper function to determine upsell product
function getUpsellProduct(purchasedProductId: string): string | null {
  const upsellMap: Record<string, string> = {
    'prod_basic_plan': 'prod_pro_plan',
    'prod_starter_course': 'prod_complete_bundle',
    'prod_single_license': 'prod_team_license'
  };
  
  return upsellMap[purchasedProductId] || null;
}

app.listen(3000);

Best Practices

The best time to offer an upsell is immediately after a successful purchase when customers are in a buying mindset. Other effective moments:
  • After feature usage milestones
  • When approaching plan limits
  • During onboarding completion
Before attempting a one-click charge, verify the payment method:
  • Is compatible with the product’s currency
  • Hasn’t expired
  • Belongs to the customer
The API will validate these, but checking proactively improves UX.
When one-click charges fail:
  1. Fall back to standard checkout flow
  2. Notify the customer with clear messaging
  3. Offer to update payment method
  4. Don’t repeatedly attempt failed charges
Upsells convert better when customers understand the value:
  • Show what they’re getting vs. current plan
  • Highlight the price difference, not total price
  • Use social proof (e.g., “Most popular upgrade”)
  • Always provide an easy way to decline
  • Don’t show the same upsell repeatedly after decline
  • Track and analyze which upsells convert to optimize offers

Webhooks to Monitor

Track these webhook events for upsell and downgrade flows:
EventTriggerAction
payment.succeededUpsell/cross-sell payment completedDeliver product, update access
payment.failedOne-click charge failedShow error, offer retry or fallback
subscription.plan_changedUpgrade/downgrade completedUpdate features, send confirmation
subscription.activeSubscription reactivated after plan changeGrant access to new tier

Webhook Integration Guide

Learn how to set up and verify webhook endpoints.