Dub 是一个强大的链接管理平台,帮助您创建、分享和跟踪短链接。通过将 Dodo Payments 与 Dub 集成,您可以在客户完成购买时自动跟踪销售转化事件,使您能够衡量营销活动和推荐计划的投资回报率。
当客户完成以下操作时,Dub 会记录一个 “销售” 事件:
此集成需要在您的链接上启用转化跟踪的 Dub 账户。
工作原理
Dub 通过在用户点击您的 Dub 短链接时存储在 cookie 中的唯一点击 ID (dub_id) 来跟踪访客。要将销售归因于您的链接,您需要:
- 在创建结账会话时捕获 Dub 的点击 ID,从
dub_id cookie 中获取
- 将点击 ID 存储在您的支付元数据中,连同客户的外部 ID
- 在支付成功时通过其 Track API 发送销售数据到 Dub
这使得 Dub 能够将成功的销售与原始链接点击匹配,从而为您提供完整的转化归因。
前提条件
在设置此集成之前,请确保您拥有:
- 一个 Dub 账户 和一个工作区
- 为您的链接启用转化跟踪
- 您的 Dub API 密钥(在您的 Dub 仪表板的设置 → API 密钥中可用)
开始使用
在 Dub 中启用转化跟踪
在您的 Dub 仪表板中,为您想要跟踪销售的链接启用转化跟踪。这使得 Dub 能够在客户完成购买时记录销售事件。 获取您的 Dub API 密钥
导航到您的 Dub 仪表板 → 设置 → API 密钥,并创建一个具有 conversions.write 范围的新 API 密钥。保持您的 API 密钥安全,切勿在客户端代码中暴露它。
在结账中捕获点击 ID
在创建结账会话时,从 cookie 中捕获 Dub 点击 ID,并将其添加到您的支付元数据中。
通过 Webhook 发送销售数据
配置一个 webhook,在支付成功时将销售数据发送到 Dub 的 Track API。
完成!
销售转化事件现在将在您的 Dub 分析仪表板中显示,并完全归因于您的链接。
实施指南
第 1 步:将点击 ID 和客户 ID 添加到结账元数据
在创建结账会话时,从 cookie 中捕获 Dub 点击 ID,并将其与客户的外部 ID 一起包含在您的支付元数据中。
import { cookies } from 'next/headers';
import DodoPayments from 'dodopayments';
const client = new DodoPayments();
export async function createCheckout(productId: string, customerId: string) {
// Capture Dub click ID from cookie
const dubClickId = cookies().get('dub_id')?.value;
const payment = await client.payments.create({
billing: {
city: 'New York',
country: 'US',
state: 'NY',
street: '123 Main St',
zipcode: '10001',
},
customer: {
email: '[email protected]',
name: 'John Doe',
},
product_id: productId,
metadata: {
dub_click_id: dubClickId, // Store Dub click ID
dub_external_id: customerId, // Store your customer's unique ID
},
});
return payment;
}
第 2 步:将销售数据发送到 Dub
配置一个 webhook 端点,在支付成功时将销售数据发送到 Dub 的 Track API。
打开 Webhook 部分
在您的 Dodo Payments 仪表板中,导航到 Webhooks → + 添加端点 并展开集成下拉菜单。 配置转换
编辑转换代码以格式化支付数据以适应 Dub 的 Track Sale API。
测试并创建
使用示例有效负载进行测试,然后单击 创建 以激活集成。
转换代码示例
基本销售跟踪
在支付成功时跟踪销售:
function handler(webhook) {
if (webhook.eventType === "payment.succeeded") {
const payment = webhook.payload.data;
// Only send to Dub if click ID exists in metadata
if (payment.metadata && payment.metadata.dub_click_id) {
webhook.payload = {
clickId: payment.metadata.dub_click_id,
externalId: payment.metadata.dub_external_id || payment.customer.customer_id,
amount: payment.total_amount, // Ensure the amount is in cents
currency: payment.currency || "USD",
paymentProcessor: "dodo",
invoiceId: payment.payment_id,
metadata: {
customer_email: payment.customer.email,
customer_name: payment.customer.name,
product_id: payment.product_cart ? payment.product_cart.map(product => product.product_id).join(', ') : undefined,
},
};
} else {
// Cancel dispatch if no click ID (organic traffic)
webhook.cancel = true;
}
}
return webhook;
}
跟踪订阅销售
跟踪初始订阅和定期付款:
function handler(webhook) {
const data = webhook.payload.data;
// Track initial subscription activation
if (webhook.eventType === "subscription.active") {
if (data.metadata && data.metadata.dub_click_id) {
webhook.payload = {
clickId: data.metadata.dub_click_id,
externalId: data.metadata.dub_external_id || data.customer.customer_id,
amount: data.recurring_pre_tax_amount, // Amount in cents
currency: data.currency || "USD",
paymentProcessor: "dodo",
invoiceId: data.subscription_id,
eventName: "Subscription Started",
metadata: {
subscription_id: data.subscription_id,
product_id: data.product_id,
billing_interval: data.payment_frequency_interval,
customer_email: data.customer.email,
},
};
} else {
// Cancel dispatch if no click ID (organic traffic)
webhook.cancel = true;
}
}
// Track recurring subscription payments
if (webhook.eventType === "subscription.renewed") {
if (data.metadata && data.metadata.dub_click_id) {
webhook.payload = {
clickId: data.metadata.dub_click_id,
externalId: data.metadata.dub_external_id || data.customer.customer_id,
amount: data.recurring_pre_tax_amount,
currency: data.currency || "USD",
paymentProcessor: "dodo",
invoiceId: `${data.subscription_id}_${Date.now()}`,
eventName: "Subscription Renewed",
metadata: {
subscription_id: data.subscription_id,
product_id: data.product_id,
customer_email: data.customer.email,
},
};
} else {
// Cancel dispatch if no click ID (organic traffic)
webhook.cancel = true;
}
}
return webhook;
}
跟踪不含税销售
仅将税前金额发送到 Dub 以进行准确的收入跟踪:
function handler(webhook) {
if (webhook.eventType === "payment.succeeded") {
const payment = webhook.payload.data;
if (payment.metadata && payment.metadata.dub_click_id) {
// Calculate pre-tax amount (total minus tax)
const preTaxAmount = payment.total_amount - (payment.tax || 0);
webhook.payload = {
clickId: payment.metadata.dub_click_id,
externalId: payment.metadata.dub_external_id || payment.customer.customer_id,
amount: preTaxAmount, // Pre-tax amount in cents
currency: payment.currency || "USD",
paymentProcessor: "dodo",
invoiceId: payment.payment_id,
metadata: {
total_amount: payment.total_amount,
tax_amount: payment.tax || 0,
customer_email: payment.customer.email,
},
};
} else {
// Cancel dispatch if no click ID (organic traffic)
webhook.cancel = true;
}
}
return webhook;
}
使用自定义事件名称跟踪销售
使用自定义事件名称对不同类型的销售进行分类:
function handler(webhook) {
if (webhook.eventType === "payment.succeeded") {
const payment = webhook.payload.data;
if (payment.metadata && payment.metadata.dub_click_id) {
// Determine event name based on payment type
let eventName = "Purchase";
if (payment.subscription_id) {
eventName = "Subscription Purchase";
} else if (payment.metadata && payment.metadata.is_upgrade) {
eventName = "Plan Upgrade";
}
webhook.payload = {
clickId: payment.metadata.dub_click_id,
externalId: payment.metadata.dub_external_id || payment.customer.customer_id,
amount: payment.total_amount,
currency: payment.currency || "USD",
paymentProcessor: "dodo",
invoiceId: payment.payment_id,
eventName: eventName,
metadata: {
product_id: payment.product_cart ? payment.product_cart.map(product => product.product_id).join(', ') : undefined,
customer_email: payment.customer.email,
},
};
} else {
// Cancel dispatch if no click ID (organic traffic)
webhook.cancel = true;
}
}
return webhook;
}
替代方案:客户端实现
如果您更喜欢从服务器跟踪销售而不是使用 webhook,您可以在成功支付后直接调用 Dub 的 Track API:
'use server';
import { Dub } from 'dub';
const dub = new Dub();
export async function trackSale(
paymentId: string,
clickId: string,
customerId: string,
amount: number,
currency: string
) {
await dub.track.sale({
clickId: clickId,
externalId: customerId,
amount: amount,
currency: currency,
paymentProcessor: 'dodo',
invoiceId: paymentId,
});
}
最佳实践
尽早捕获点击 ID:在结账流程中尽早存储 Dub 点击 ID,以确保准确的归因,即使用户导航离开并稍后返回。
- 始终在元数据中包含点击 ID:没有点击 ID,Dub 无法将收入归因于您的链接
- 一致使用外部 ID:传递您在系统中使用的相同客户 ID,以便进行准确的客户级分析
- 优雅处理自然流量:在没有点击 ID 时设置
webhook.cancel = true 以避免不必要的 API 调用
- 使用示例支付进行测试:在上线之前验证集成是否正常工作
- 监控您的 Dub 仪表板:检查销售是否正确显示并具有适当的归因
重要说明
- 金额格式:Dub 期望金额以美分为单位(例如,$10.00 = 1000)
- 货币:使用 ISO 4217 货币代码(USD、EUR、GBP 等)
- 免费试用:$0 付款不被视为销售
- 退款:如有需要,考虑单独跟踪退款以进行准确的收入报告
故障排除
- 验证您的 Dub API 密钥是否正确,并具有
conversions.write 范围
- 检查
dub_click_id 是否被捕获并存储在支付元数据中
- 确保 webhook 转换正确格式化有效负载
- 验证 webhook 是否在
payment.succeeded 事件上触发
- 确认为您的 Dub 链接启用了转化跟踪
- 确认用户在结账前是否点击了您的 Dub 短链接
- 验证
dub_id cookie 是否在您的域上正确设置
- 检查点击 ID 是否在结账创建和支付完成之间匹配
- 确保您在创建结账会话之前捕获点击 ID
- 验证 JSON 结构是否符合 Dub 的 Track Sale API 格式
- 检查所有必需字段(
clickId, externalId, amount)是否存在
- 确保金额以美分为单位(整数,而不是小数)
- 验证 API 端点 URL 是否正确:
https://api.dub.co/track/sale
- 使用示例 webhook 有效负载测试转换
- 确保您仅在
payment.succeeded 事件上进行跟踪,而不是 payment.processing
- 为每笔销售使用唯一的
invoiceId 值
- 对于订阅,在续订时附加时间戳或计费周期以防止重复
其他资源