polar-migration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePolar Migration Guide
Polar迁移指南
Migrate to Polar from other payment platforms while minimizing disruption to your customers and revenue.
从其他支付平台迁移至Polar,同时将对客户和收入的影响降至最低。
Migration Overview
迁移概述
What Can Be Migrated
可迁移内容
| Data Type | Migration Method |
|---|---|
| Customer emails/names | API import |
| Product catalog | Manual recreation |
| Active subscriptions | New subscription creation |
| Payment methods | Customer re-entry required* |
| Historical orders | Not migrated (keep in old system) |
| License keys | New keys generated |
*Payment method tokens cannot be transferred between processors due to PCI compliance. Customers will need to re-enter payment details.
| 数据类型 | 迁移方式 |
|---|---|
| 客户邮箱/姓名 | API导入 |
| 产品目录 | 手动重建 |
| 活跃订阅 | 创建新订阅 |
| 支付方式 | 需要客户重新输入* |
| 历史订单 | 不迁移(保留在旧系统中) |
| 许可证密钥 | 生成新密钥 |
由于PCI合规要求,支付方式令牌无法在不同处理器之间转移。客户需要重新输入支付信息。
Migration Strategies
迁移策略
1. Hard Cutover
- Cancel all subscriptions in old system
- Create new subscriptions in Polar
- Best for: Small customer base, simple products
2. Gradual Migration
- Run both systems in parallel
- Migrate customers as they renew
- Best for: Large customer base, minimizing risk
3. New Customers Only
- Keep existing customers on old system
- New customers use Polar
- Best for: Testing Polar before full commitment
1. 直接切换
- 取消旧系统中的所有订阅
- 在Polar中创建新订阅
- 适用场景:客户群体小、产品结构简单
2. 逐步迁移
- 并行运行两个系统
- 客户续订时进行迁移
- 适用场景:客户群体大、需最小化风险
3. 仅新客户使用
- 现有客户保留在旧系统
- 新客户使用Polar
- 适用场景:全面投入前测试Polar
Pre-Migration Checklist
迁移前检查清单
1. Set Up Polar
1. 设置Polar
bash
undefinedbash
undefinedTest in sandbox first
先在沙箱环境测试
Install SDK
安装SDK
npm install @polar-sh/sdk
undefinednpm install @polar-sh/sdk
undefined2. Map Your Products
2. 映射产品
Create equivalent products in Polar:
typescript
import { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "sandbox", // Test first!
});
// Create products matching your old catalog
const basicPlan = await polar.products.create({
name: "Basic Plan",
organizationId: "org_xxx",
description: "Basic features",
prices: [
{
type: "recurring",
recurringInterval: "month",
amountType: "fixed",
priceAmount: 900, // $9.00
priceCurrency: "usd",
},
{
type: "recurring",
recurringInterval: "year",
amountType: "fixed",
priceAmount: 9000, // $90.00
priceCurrency: "usd",
},
],
});在Polar中创建对应的产品:
typescript
import { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "sandbox", // 先测试!
});
// 创建与旧目录匹配的产品
const basicPlan = await polar.products.create({
name: "Basic Plan",
organizationId: "org_xxx",
description: "Basic features",
prices: [
{
type: "recurring",
recurringInterval: "month",
amountType: "fixed",
priceAmount: 900, // $9.00
priceCurrency: "usd",
},
{
type: "recurring",
recurringInterval: "year",
amountType: "fixed",
priceAmount: 9000, // $90.00
priceCurrency: "usd",
},
],
});3. Map Benefits/Entitlements
3. 映射权益/权限
typescript
// Create benefits in Polar
const licenseBenefit = await polar.benefits.create({
type: "license_keys",
description: "Software License",
organizationId: "org_xxx",
properties: {
prefix: "PRO",
activations: { limit: 3 },
},
});
// Attach the benefit to the product in the Polar dashboard
// (Organization → Products → [product] → Benefits).typescript
// 在Polar中创建权益
const licenseBenefit = await polar.benefits.create({
type: "license_keys",
description: "Software License",
organizationId: "org_xxx",
properties: {
prefix: "PRO",
activations: { limit: 3 },
},
});
// 在Polar控制台中将权益关联到产品
// (组织 → 产品 → [对应产品] → 权益)4. Set Up Webhooks
4. 设置Webhook
typescript
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
export async function POST(request: Request): Promise<Response> {
const body = await request.text();
let event: ReturnType<typeof validateEvent>;
try {
event = validateEvent(
body,
{
"webhook-id": request.headers.get("webhook-id") ?? "",
"webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
"webhook-signature": request.headers.get("webhook-signature") ?? "",
},
process.env.POLAR_WEBHOOK_SECRET!,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return Response.json({ received: false }, { status: 403 });
}
throw error;
}
switch (event.type) {
case "order.paid":
// Grant access in your system
await grantAccess(event.data.customer.externalId, event.data.productId);
break;
case "subscription.canceled":
// Schedule access removal
await scheduleAccessRemoval(event.data.customer.externalId, event.data.endsAt);
break;
}
return Response.json({ received: true });
}See the skill for the full webhook recipe (event types, framework variations, raw-body requirement, idempotency).
polar-integrationtypescript
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
export async function POST(request: Request): Promise<Response> {
const body = await request.text();
let event: ReturnType<typeof validateEvent>;
try {
event = validateEvent(
body,
{
"webhook-id": request.headers.get("webhook-id") ?? "",
"webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
"webhook-signature": request.headers.get("webhook-signature") ?? "",
},
process.env.POLAR_WEBHOOK_SECRET!,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return Response.json({ received: false }, { status: 403 });
}
throw error;
}
switch (event.type) {
case "order.paid":
// 在你的系统中授予权限
await grantAccess(event.data.customer.externalId, event.data.productId);
break;
case "subscription.canceled":
// 安排权限移除
await scheduleAccessRemoval(event.data.customer.externalId, event.data.endsAt);
break;
}
return Response.json({ received: true });
}查看技能获取完整的Webhook方案(事件类型、框架变体、原始数据要求、幂等性)。
polar-integrationMigration from Stripe Billing
从Stripe Billing迁移
Export Customer Data
导出客户数据
typescript
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Export customers with active subscriptions
async function exportStripeCustomers() {
const customers: ExportedCustomer[] = [];
for await (const subscription of stripe.subscriptions.list({ status: "active" })) {
const customer = await stripe.customers.retrieve(subscription.customer as string);
if (customer.deleted) continue;
customers.push({
stripeCustomerId: customer.id,
email: customer.email!,
name: customer.name || undefined,
subscriptionId: subscription.id,
productId: subscription.items.data[0].price.product as string,
priceId: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
});
}
return customers;
}typescript
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// 导出带有活跃订阅的客户
async function exportStripeCustomers() {
const customers: ExportedCustomer[] = [];
for await (const subscription of stripe.subscriptions.list({ status: "active" })) {
const customer = await stripe.customers.retrieve(subscription.customer as string);
if (customer.deleted) continue;
customers.push({
stripeCustomerId: customer.id,
email: customer.email!,
name: customer.name || undefined,
subscriptionId: subscription.id,
productId: subscription.items.data[0].price.product as string,
priceId: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
});
}
return customers;
}Create Customers in Polar
在Polar中创建客户
typescript
// Map Stripe products to Polar products
const productMap: Record<string, string> = {
"prod_stripe_basic": "prod_polar_basic",
"prod_stripe_pro": "prod_polar_pro",
};
async function createPolarCustomers(customers: ExportedCustomer[]) {
for (const customer of customers) {
// Create customer in Polar with external ID for linking
const polarCustomer = await polar.customers.create({
organizationId: "org_xxx",
email: customer.email,
name: customer.name,
externalId: customer.stripeCustomerId, // Link to Stripe ID
});
console.log(`Created Polar customer: ${polarCustomer.id} for ${customer.email}`);
}
}typescript
// 映射Stripe产品到Polar产品
const productMap: Record<string, string> = {
"prod_stripe_basic": "prod_polar_basic",
"prod_stripe_pro": "prod_polar_pro",
};
async function createPolarCustomers(customers: ExportedCustomer[]) {
for (const customer of customers) {
// 在Polar中创建客户并设置外部ID用于关联
const polarCustomer = await polar.customers.create({
organizationId: "org_xxx",
email: customer.email,
name: customer.name,
externalId: customer.stripeCustomerId, // 关联到Stripe ID
});
console.log(`Created Polar customer: ${polarCustomer.id} for ${customer.email}`);
}
}Migrate Subscriptions (Gradual)
逐步迁移订阅
typescript
async function migrateSubscription(customer: ExportedCustomer) {
const polarProductId = productMap[customer.productId];
// Create checkout for customer to enter new payment method
const checkout = await polar.checkouts.create({
products: [polarProductId],
customerEmail: customer.email,
externalCustomerId: customer.stripeCustomerId,
successUrl: `https://yoursite.com/migration-complete?customer=${customer.stripeCustomerId}`,
// Allow discount for migration
discountId: "migration_discount_xxx",
});
// Send migration email to customer
await sendMigrationEmail(customer.email, {
checkoutUrl: checkout.url,
currentPeriodEnd: customer.currentPeriodEnd,
});
return checkout;
}typescript
async function migrateSubscription(customer: ExportedCustomer) {
const polarProductId = productMap[customer.productId];
// 创建结账链接供客户输入新支付方式
const checkout = await polar.checkouts.create({
products: [polarProductId],
customerEmail: customer.email,
externalCustomerId: customer.stripeCustomerId,
successUrl: `https://yoursite.com/migration-complete?customer=${customer.stripeCustomerId}`,
// 提供迁移折扣
discountId: "migration_discount_xxx",
});
// 向客户发送迁移邮件
await sendMigrationEmail(customer.email, {
checkoutUrl: checkout.url,
currentPeriodEnd: customer.currentPeriodEnd,
});
return checkout;
}Handle Migration Completion
处理迁移完成
typescript
// Inside the validateEvent switch in your webhook handler:
case "order.paid": {
const order = event.data;
const stripeCustomerId = order.customer.externalId;
if (stripeCustomerId?.startsWith("cus_")) {
// Cancel Stripe subscription at period end
const stripeSubscriptions = await stripe.subscriptions.list({
customer: stripeCustomerId,
status: "active",
});
for (const sub of stripeSubscriptions.data) {
await stripe.subscriptions.update(sub.id, {
cancel_at_period_end: true,
});
}
// Mark customer as migrated in your database
await db.customer.update({
where: { stripeId: stripeCustomerId },
data: {
migratedToPolar: true,
polarCustomerId: order.customer.id,
},
});
}
break;
}typescript
// 在Webhook处理器的validateEvent分支中:
case "order.paid": {
const order = event.data;
const stripeCustomerId = order.customer.externalId;
if (stripeCustomerId?.startsWith("cus_")) {
// 到期时取消Stripe订阅
const stripeSubscriptions = await stripe.subscriptions.list({
customer: stripeCustomerId,
status: "active",
});
for (const sub of stripeSubscriptions.data) {
await stripe.subscriptions.update(sub.id, {
cancel_at_period_end: true,
});
}
// 在数据库中标记客户已迁移
await db.customer.update({
where: { stripeId: stripeCustomerId },
data: {
migratedToPolar: true,
polarCustomerId: order.customer.id,
},
});
}
break;
}Migration from Paddle
从Paddle迁移
Export Paddle Data
导出Paddle数据
typescript
// Paddle API to export subscribers
async function exportPaddleSubscribers() {
const response = await fetch("https://vendors.paddle.com/api/2.0/subscription/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
vendor_id: process.env.PADDLE_VENDOR_ID,
vendor_auth_code: process.env.PADDLE_AUTH_CODE,
state: "active",
}),
});
const data = await response.json();
return data.response;
}typescript
// 使用Paddle API导出订阅用户
async function exportPaddleSubscribers() {
const response = await fetch("https://vendors.paddle.com/api/2.0/subscription/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
vendor_id: process.env.PADDLE_VENDOR_ID,
vendor_auth_code: process.env.PADDLE_AUTH_CODE,
state: "active",
}),
});
const data = await response.json();
return data.response;
}Map Paddle to Polar
映射Paddle到Polar
typescript
const paddleToPolar = {
products: {
"paddle_12345": "polar_basic",
"paddle_12346": "polar_pro",
},
};
async function migratePaddleCustomer(paddleUser: PaddleSubscriber) {
// Create checkout with Polar
const checkout = await polar.checkouts.create({
products: [paddleToPolar.products[paddleUser.plan_id]],
customerEmail: paddleUser.user_email,
externalCustomerId: `paddle_${paddleUser.user_id}`,
successUrl: "https://yoursite.com/migrated",
});
return checkout;
}typescript
const paddleToPolar = {
products: {
"paddle_12345": "polar_basic",
"paddle_12346": "polar_pro",
},
};
async function migratePaddleCustomer(paddleUser: PaddleSubscriber) {
// 创建Polar结账链接
const checkout = await polar.checkouts.create({
products: [paddleToPolar.products[paddleUser.plan_id]],
customerEmail: paddleUser.user_email,
externalCustomerId: `paddle_${paddleUser.user_id}`,
successUrl: "https://yoursite.com/migrated",
});
return checkout;
}Migration from Lemon Squeezy
从Lemon Squeezy迁移
typescript
// Lemon Squeezy API export
async function exportLemonSqueezyCustomers() {
const response = await fetch("https://api.lemonsqueezy.com/v1/subscriptions", {
headers: {
Authorization: `Bearer ${process.env.LEMON_SQUEEZY_API_KEY}`,
},
});
const { data } = await response.json();
return data.filter((sub: any) => sub.attributes.status === "active");
}typescript
// Lemon Squeezy API导出
async function exportLemonSqueezyCustomers() {
const response = await fetch("https://api.lemonsqueezy.com/v1/subscriptions", {
headers: {
Authorization: `Bearer ${process.env.LEMON_SQUEEZY_API_KEY}`,
},
});
const { data } = await response.json();
return data.filter((sub: any) => sub.attributes.status === "active");
}Customer Communication
客户沟通
Migration Email Template
迁移邮件模板
typescript
const migrationEmailTemplate = {
subject: "Action Required: Update Your Payment Method",
body: `
Hi {{customer_name}},
We're upgrading our payment system to provide you with a better experience.
**What you need to do:**
Click the link below to update your payment method. This will only take a minute.
[Update Payment Method]({{checkout_url}})
**What's changing:**
- More payment options
- Better invoice management
- Improved customer portal
**What's NOT changing:**
- Your subscription price
- Your features and access
- Your billing date
**Timeline:**
Please complete this by {{deadline}} to ensure uninterrupted service.
Your current subscription will continue until {{current_period_end}}, then
seamlessly transition to our new system.
Questions? Reply to this email.
Thanks,
{{company_name}}
`,
};
async function sendMigrationEmail(email: string, data: MigrationData) {
// Use your email service (SendGrid, Postmark, etc.)
await emailService.send({
to: email,
subject: migrationEmailTemplate.subject,
body: renderTemplate(migrationEmailTemplate.body, {
customer_name: data.name || "there",
checkout_url: data.checkoutUrl,
deadline: formatDate(data.deadline),
current_period_end: formatDate(data.currentPeriodEnd),
company_name: "Your Company",
}),
});
}typescript
const migrationEmailTemplate = {
subject: "Action Required: Update Your Payment Method",
body: `
Hi {{customer_name}},
We're upgrading our payment system to provide you with a better experience.
**What you need to do:**
Click the link below to update your payment method. This will only take a minute.
[Update Payment Method]({{checkout_url}})
**What's changing:**
- More payment options
- Better invoice management
- Improved customer portal
**What's NOT changing:**
- Your subscription price
- Your features and access
- Your billing date
**Timeline:**
Please complete this by {{deadline}} to ensure uninterrupted service.
Your current subscription will continue until {{current_period_end}}, then
seamlessly transition to our new system.
Questions? Reply to this email.
Thanks,
{{company_name}}
`,
};
async function sendMigrationEmail(email: string, data: MigrationData) {
// 使用你的邮件服务(SendGrid、Postmark等)
await emailService.send({
to: email,
subject: migrationEmailTemplate.subject,
body: renderTemplate(migrationEmailTemplate.body, {
customer_name: data.name || "there",
checkout_url: data.checkoutUrl,
deadline: formatDate(data.deadline),
current_period_end: formatDate(data.currentPeriodEnd),
company_name: "Your Company",
}),
});
}Reminder Sequence
提醒序列
typescript
const reminderSchedule = [
{ daysBeforeDeadline: 14, template: "initial" },
{ daysBeforeDeadline: 7, template: "reminder" },
{ daysBeforeDeadline: 3, template: "urgent" },
{ daysBeforeDeadline: 1, template: "final" },
];
async function scheduleReminders(customer: Customer, deadline: Date) {
for (const reminder of reminderSchedule) {
const sendAt = subDays(deadline, reminder.daysBeforeDeadline);
await scheduleEmail({
to: customer.email,
template: reminder.template,
sendAt,
data: { checkoutUrl: customer.migrationCheckoutUrl },
});
}
}typescript
const reminderSchedule = [
{ daysBeforeDeadline: 14, template: "initial" },
{ daysBeforeDeadline: 7, template: "reminder" },
{ daysBeforeDeadline: 3, template: "urgent" },
{ daysBeforeDeadline: 1, template: "final" },
];
async function scheduleReminders(customer: Customer, deadline: Date) {
for (const reminder of reminderSchedule) {
const sendAt = subDays(deadline, reminder.daysBeforeDeadline);
await scheduleEmail({
to: customer.email,
template: reminder.template,
sendAt,
data: { checkoutUrl: customer.migrationCheckoutUrl },
});
}
}Parallel Running
并行运行
Router Pattern
路由模式
typescript
// Determine which system handles a customer
async function getPaymentSystem(userId: string): Promise<"stripe" | "polar"> {
const user = await db.user.findUnique({ where: { id: userId } });
if (user?.polarCustomerId) {
return "polar";
}
return "stripe";
}
// Route subscription checks
async function hasActiveSubscription(userId: string): Promise<boolean> {
const system = await getPaymentSystem(userId);
if (system === "polar") {
const state = await polar.customers.getState({
customerId: user.polarCustomerId,
});
return state.activeSubscriptions.length > 0;
} else {
const subscriptions = await stripe.subscriptions.list({
customer: user.stripeCustomerId,
status: "active",
});
return subscriptions.data.length > 0;
}
}typescript
// 确定哪个系统处理客户请求
async function getPaymentSystem(userId: string): Promise<"stripe" | "polar"> {
const user = await db.user.findUnique({ where: { id: userId } });
if (user?.polarCustomerId) {
return "polar";
}
return "stripe";
}
// 路由订阅检查
async function hasActiveSubscription(userId: string): Promise<boolean> {
const system = await getPaymentSystem(userId);
if (system === "polar") {
const state = await polar.customers.getState({
customerId: user.polarCustomerId,
});
return state.activeSubscriptions.length > 0;
} else {
const subscriptions = await stripe.subscriptions.list({
customer: user.stripeCustomerId,
status: "active",
});
return subscriptions.data.length > 0;
}
}Unified Webhook Handler
统一Webhook处理器
typescript
// Handle webhooks from both systems during migration
app.post("/webhooks/stripe", stripeWebhookHandler);
app.post("/webhooks/polar", polarWebhookHandler);
// Normalize events to your internal format
interface SubscriptionEvent {
type: "created" | "canceled" | "renewed";
customerId: string;
productId: string;
source: "stripe" | "polar";
}
function normalizeStripeEvent(event: Stripe.Event): SubscriptionEvent | null {
// Convert Stripe event to internal format
}
function normalizePolarEvent(event: PolarWebhookEvent): SubscriptionEvent | null {
// Convert Polar event to internal format
}typescript
// 迁移期间处理来自两个系统的Webhook
app.post("/webhooks/stripe", stripeWebhookHandler);
app.post("/webhooks/polar", polarWebhookHandler);
// 将事件标准化为内部格式
interface SubscriptionEvent {
type: "created" | "canceled" | "renewed";
customerId: string;
productId: string;
source: "stripe" | "polar";
}
function normalizeStripeEvent(event: Stripe.Event): SubscriptionEvent | null {
// 将Stripe事件转换为内部格式
}
function normalizePolarEvent(event: PolarWebhookEvent): SubscriptionEvent | null {
// 将Polar事件转换为内部格式
}Post-Migration Cleanup
迁移后清理
Verify Migration
验证迁移
typescript
async function verifyMigration() {
// Get all customers marked as migrated
const migratedCustomers = await db.customer.findMany({
where: { migratedToPolar: true },
});
const issues: string[] = [];
for (const customer of migratedCustomers) {
// Verify Polar subscription exists
const polarState = await polar.customers.getState({
customerId: customer.polarCustomerId,
});
if (polarState.activeSubscriptions.length === 0) {
issues.push(`${customer.email}: No active Polar subscription`);
}
// Verify Stripe subscription is canceled
const stripeSubscriptions = await stripe.subscriptions.list({
customer: customer.stripeCustomerId,
status: "active",
});
if (stripeSubscriptions.data.length > 0) {
issues.push(`${customer.email}: Stripe subscription still active`);
}
}
return issues;
}typescript
async function verifyMigration() {
// 获取所有标记为已迁移的客户
const migratedCustomers = await db.customer.findMany({
where: { migratedToPolar: true },
});
const issues: string[] = [];
for (const customer of migratedCustomers) {
// 验证Polar订阅是否存在
const polarState = await polar.customers.getState({
customerId: customer.polarCustomerId,
});
if (polarState.activeSubscriptions.length === 0) {
issues.push(`${customer.email}: No active Polar subscription`);
}
// 验证Stripe订阅已取消
const stripeSubscriptions = await stripe.subscriptions.list({
customer: customer.stripeCustomerId,
status: "active",
});
if (stripeSubscriptions.data.length > 0) {
issues.push(`${customer.email}: Stripe subscription still active`);
}
}
return issues;
}Cancel Old System
取消旧系统服务
typescript
// After all customers migrated, cancel remaining Stripe subscriptions
async function cancelRemainingStripeSubscriptions() {
for await (const subscription of stripe.subscriptions.list({ status: "active" })) {
// Check if customer was migrated
const customer = await db.customer.findFirst({
where: { stripeId: subscription.customer as string },
});
if (customer?.migratedToPolar) {
await stripe.subscriptions.cancel(subscription.id);
console.log(`Canceled Stripe subscription ${subscription.id}`);
}
}
}typescript
// 所有客户迁移完成后,取消剩余的Stripe订阅
async function cancelRemainingStripeSubscriptions() {
for await (const subscription of stripe.subscriptions.list({ status: "active" })) {
// 检查客户是否已迁移
const customer = await db.customer.findFirst({
where: { stripeId: subscription.customer as string },
});
if (customer?.migratedToPolar) {
await stripe.subscriptions.cancel(subscription.id);
console.log(`Canceled Stripe subscription ${subscription.id}`);
}
}
}Migration Checklist
迁移检查清单
Before Migration
迁移前
- Products created in Polar (sandbox)
- Benefits/entitlements mapped
- Webhooks configured
- Email templates ready
- Customer support informed
- Rollback plan documented
- 在Polar(沙箱)中创建产品
- 映射权益/权限
- 配置Webhook
- 准备邮件模板
- 告知客户支持团队
- 记录回滚计划
During Migration
迁移中
- Test with small batch first
- Monitor error rates
- Track conversion rate
- Respond to customer issues quickly
- Send reminder emails on schedule
- 先使用小批量测试
- 监控错误率
- 跟踪转化率
- 快速响应客户问题
- 按计划发送提醒邮件
After Migration
迁移后
- Verify all subscriptions migrated
- Cancel old platform subscriptions
- Update documentation
- Remove old platform code
- Archive old platform data
- Celebrate! 🎉
- 验证所有订阅已迁移
- 取消旧平台订阅
- 更新文档
- 移除旧平台代码
- 归档旧平台数据
- 庆祝!🎉
Troubleshooting
故障排除
Customer didn't receive migration email
- Check spam folder
- Verify email address in export
- Resend with different subject line
Customer completed checkout but Stripe not canceled
- Check webhook logs
- Verify external_id mapping
- Manually cancel if needed
Double billing
- Refund on old platform
- Apologize to customer
- Improve timing logic
Customer wants to stay on old platform
- Respect their choice (if possible)
- Offer incentive to migrate
- Set final deadline
客户未收到迁移邮件
- 检查垃圾邮件文件夹
- 验证导出的邮箱地址
- 使用不同主题重新发送
客户完成结账但Stripe订阅未取消
- 检查Webhook日志
- 验证external_id映射
- 必要时手动取消
重复计费
- 在旧平台退款
- 向客户致歉
- 改进时间逻辑
客户希望保留在旧平台
- 尊重其选择(如可行)
- 提供迁移激励
- 设置最终截止日期