跳转到主要内容
追加销售和降级销售允许您使用客户已保存的支付方式提供附加产品或计划更改。这使得可以实现跳过支付收集的一键购买,从而显著提高转化率。

Post-Purchase Upsells

使用一键购买在结账后立即提供互补产品。

Subscription Upgrades

利用自动比例分配和即时结算将客户迁移到更高层级。

Cross-Sells

向现有客户添加相关产品,无需重新输入支付信息。

概述

追加销售和降级销售是强大的营收优化策略:
  • 追加销售:提供更高价值的产品或升级(例如将基本计划升级到专业计划)
  • 降级销售:当客户拒绝或降级时提供低价替代选项
  • 交叉销售:建议互补产品(例如附加组件、相关商品)
Dodo Payments 通过 payment_method_id 参数启用这些流程,该参数使您可以在无需客户重新输入卡片详细信息的情况下收取其已保存的支付方式。

主要收益

收益影响
一键购买完全跳过返回客户的支付表单
更高转化率在决策时刻减少摩擦
即时处理使用 confirm: true 立即处理费用
无缝用户体验客户在整个流程中始终留在您的应用内

工作原理

前提条件

在实施追加销售和降级销售之前,请确保您已完成以下准备工作:
1

Customer with Saved Payment Method

客户必须至少完成一次购买。客户完成结账后,支付方式会自动保存。
2

Products Configured

在 Dodo Payments 仪表板中创建您的追加销售产品。这些可以是一次性付款、订阅或附加组件。
3

Webhook Endpoint

设置 Webhook 以处理 payment.succeededpayment.failedsubscription.plan_changed 事件。

获取客户支付方式

在提供追加销售之前,请检索客户已保存的支付方式:
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;
支付方式在客户完成结账后会自动保存。您无需手动保存。

购后的一键追加销售

在成功购买后立即提供附加产品。客户可以一键接受,因为他们的支付方式已保存。

实施

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;
}
使用 payment_method_id 时,您必须设置 confirm: true 并提供现有的 customer_id。支付方式必须属于该客户。

订阅升级

通过自动比例分配处理,将客户迁移到更高层级的订阅计划。

承诺前预览

始终预览计划更改,让客户明确知道将被收取的费用:
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}`);

执行升级

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);

比例分配模式

选择升级时如何向客户收费:
模式行为适用场景
difference_immediately即时收取价格差(3030→80 = $50)简单升级
prorated_immediately按结算周期剩余时间计费公平的基于时间计费
full_immediately收取全新的计划价格,忽略剩余时间重置计费周期
对于直接的升级流程,请使用 difference_immediately。若想考虑当前计划尚未使用的时间,请使用 prorated_immediately

交叉销售

向现有客户添加互补产品,无需重新输入支付信息。

实施

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);
}

订阅降级

当客户希望转到更低层级计划时,请通过自动抵扣优雅处理过渡。

降级如何运作

  1. 客户申请降级(专业版 → 基础版)
  2. 系统计算当前计划剩余价值
  3. 将信用额度添加到订阅以用于未来续订
  4. 客户立即迁移到新计划
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');
使用 difference_immediately 的降级所产生的信用额度是订阅范围内的,并会自动应用于未来续订。它们不同于 Customer Credits

完整示例:购后追加销售流程

以下是一个完整实现,展示如何在成功购买后提供追加销售:
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);

最佳实践

在成功购买后立即提供追加销售是最佳时机,因为此时客户处于购买心态。其他有效时机包括:
  • 达到功能使用里程碑后
  • 接近计划额度限制时
  • 完成入职时
在尝试一键收费之前,请验证支付方式:
  • 是否与产品币种兼容
  • 是否未过期
  • 是否属于该客户
API 会验证这些,但事先检查可提升用户体验。
当一键收费失败时:
  1. 回退到标准结账流程
  2. 向客户发送清晰的提示
  3. 提供更新支付方式的选项
  4. 切勿反复尝试失败的收费
当客户了解价值时,追加销售的转化率更高:
  • 展示他们将获得的内容与当前计划的对比
  • 强调价格差异,而非总价
  • 利用社会认同(例如:“最受欢迎的升级”)
  • 始终提供简单的拒绝方式
  • 拒绝后不要反复展示相同的追加销售
  • 跟踪并分析哪些追加销售转化,以优化提案

需要监控的 Webhook

跟踪以下 Webhook 事件,以支持追加销售和降级流程:
事件触发操作
payment.succeeded追加销售/交叉销售支付完成发放产品,更新访问权限
payment.failed一键收费失败显示错误,提供重试或回退
subscription.plan_changed升级/降级完成更新功能,发送确认
subscription.active计划变更后订阅重新激活授予新层级访问权限

Webhook Integration Guide

了解如何设置和验证 Webhook 端点。

相关资源