使用 Dub 将 Stripe 自定义客户创建流程与点击归因关联:dubCustomerExternalId 与 dubClickId 元数据实战指南
【免费下载链接】dubThe modern link attribution platform. Loved by world-class marketing teams like Framer, Perplexity, Superhuman, Twilio, Buffer and more.项目地址: https://gitcode.com/GitHub_Trending/du/dub
导读
本文面向使用 Stripe 但不走 Checkout Session 标准创建流程、而是自行调用stripe.customers.create/stripe.customers.update管理客户的开发者,讲解如何在 Stripe 客户对象上写入 Dub 归因元数据(dubCustomerExternalId、dubClickId),让 Dub 在客户产生购买后自动把订单金额、币种等信息回填到用户最初点击的链接(Click Event)上,从而实现完整的"点击 → 线索 → 销售"归因闭环。读完本文,你将掌握 Dub Stripe 集成的客户侧接入方式、元数据字段的确切语义,以及 Dub 服务端 Webhook 处理这些元数据时的完整解析与关联逻辑。
一、为什么要在 Stripe 客户上写入 Dub 元数据
Dub 的 Stripe 集成核心机制是:通过 Webhook 监听 Stripe 侧的事件,再把事件中的金额、币种等销售信息关联回 Dub 侧已经记录的点击事件。而关联的"锚点"就是 Stripe 客户对象上的metadata字段。
在标准流程(stripe-checkout.md)中,你只需在checkout.sessions.create时把用户 ID 写入metadata.dubCustomerExternalId,Dub 就能在checkout.session.completed事件到达时完成归因。但如果你不使用 Checkout Session,而是直接在服务端创建 Stripe 客户(例如自有支付页面、自定义订阅管理、B2B 记账等场景),就必须在客户创建/更新流程中主动传递归因信息——这正是本文所依托文档 stripe-customers.md 的适用场景。
二、核心概念:两个元数据字段
在 Stripe 客户对象的metadata字段中,需要写入两个 Dub 约定的键:
| 元数据键 | 含义 | 来源 | 用途 |
|---|---|---|---|
dubCustomerExternalId | 你数据库中该用户的唯一用户 ID | 你的业务数据库(如user.id) | 让 Dub 在本地Customer记录与 Stripe 客户之间建立一一对应关系 |
dubClickId | 用户在落地页产生点击事件时下发的dub_id | HTTP 请求头dub_id(由 Dub 的点击跟踪脚本或服务端 SDK 下发) | 让 Dub 把新客户关联回最初带来该访客的链接与点击事件 |
需要特别说明的是:只有dubCustomerExternalId是必须的;dubClickId的作用是在客户首次出现时找到对应的点击事件,从而把客户"挂"到某个链接/合作伙伴上。从 sync-customer.ts/api/stripe/integration/webhook/utils/sync-customer.ts#L82-L96) 的源码可以看到,如果 metadata 中没有dubClickId,Webhook 会返回"Click ID not found in Stripe customer metadata, skipping..."并跳过新客户创建;而对于已存在的 Dub 客户,即使没有dubClickId也能正常更新。
三、获取点击 ID(dub_id)
dub_id是 Dub 在每次链接点击事件中生成的唯一标识,通过请求头dub_id随用户请求一起下发到你的服务端。在 Node.js 服务中读取方式如下:
const dub_id = req.headers.get("dub_id");它通常配合你现有的会话体系(Cookie、JWT 等)一起使用:用户在落地页被 Dub 跟踪后,后续到达你的支付/注册接口时,请求头中就会携带该用户来源对应的dub_id。
四、在 Stripe 客户创建流程中写入元数据
当你的后端调用stripe.customers.create创建客户时,将用户 ID 作为dubCustomerExternalId、请求头中的dub_id作为dubClickId一起写入metadata:
import { stripe } from "@/lib/stripe"; const user = { id: "user_123", email: "user@example.com", teamId: "team_xxxxxxxxx", }; const dub_id = req.headers.get("dub_id"); await stripe.customers.create({ email: user.email, name: user.name, metadata: { dubCustomerExternalId: user.id, dubClickId: dub_id, }, });email、name可保持你原有逻辑不变;dubCustomerExternalId建议使用数据库中的主键字符串(如user_123),保证全局唯一、稳定不变;dubClickId直接透传请求头dub_id即可,缺失时 Dub 端会跳过新客户创建(详见下文源码解析)。
五、在 Stripe 客户更新流程中补充元数据
如果你的应用是先创建 Stripe 客户、后补录归因信息(例如用户在注册后一段时间才通过推广链接完成转化),可以在stripe.customers.update中补齐同样的两个字段:
import { stripe } from "@/lib/stripe"; const user = { id: "user_123", email: "user@example.com", teamId: "team_xxxxxxxxx", }; const dub_id = req.headers.get("dub_id"); await stripe.customers.update(user.id, { metadata: { dubCustomerExternalId: user.id, dubClickId: dub_id, }, });由于 Stripe 的metadata是整体合并写入的,更新时只需携带归因相关键即可,不会覆盖你已有的其他业务元数据。
六、源码级原理:Dub 如何消费这些元数据
6.1 Webhook 入口与事件类型
Dub 的 Stripe 集成 Webhook 位于 apps/web/app/(ee)/api/stripe/integration/webhook/api/stripe/integration/webhook),其中sync-customer.ts专门处理customer.created与customer.updated两类事件。这两个事件正是触发"客户关联"的时机:客户创建/更新时,Dub 会读取客户对象上的metadata。
6.2 外部 ID 的解析优先级
在 get-dub-customer-external-id-from-metadata.ts/api/stripe/integration/webhook/utils/get-dub-customer-external-id-from-metadata.ts#L8-L21) 中,Dub 从 metadata 解析外部 ID 的顺序是:
dubCustomerExternalId(本指南推荐的主键字段)dubCustomerId(旧版/兼容字段)user_id(Lemon Squeezy 客户迁移到 Stripe 后的兼容字段)
解析结果为空字符串或null时返回undefined,随后sync-customer.ts会返回"External ID not found in Stripe customer metadata, skipping..."直接跳过。
6.3 客户查找、更新与创建的完整流程
sync-customer.ts的实际处理链路如下:
- 从事件对象中取出
stripeCustomer.metadata,解析dubCustomerExternalId与dubClickId; - 用
OR条件在 Dub 数据库中查找客户:(projectId + externalId)或stripeCustomerId; - 若已存在:直接更新该客户的
stripeCustomerId、projectConnectId(Stripe Account ID)、externalId,并在 Stripe 提供非空值时才覆盖name/email; - 若不存在:必须存在
dubClickId才会继续——通过 Tinybird 查询点击事件(getClickEvent),校验该链接归属同一 workspace,然后调用getOrCreateCustomer创建 Dub 客户(cus_前缀 ID),并继承点击事件中的linkId、programId、partnerId、clickId、clickedAt、country等字段; - 新客户创建成功后,记录一条名为
New customer的 Lead 事件,同时递增链接的leads计数、workspace 的usage,并异步触发工作流(leadRecorded)、合作伙伴 Postback(lead.created)、workspace Webhook(lead.created)与 Google Ads 转化上传。
并发场景下若两个 Webhook 同时创建同一客户,getOrCreateCustomer的findMode: "first"逻辑会保证只创建一个,另一个走更新分支。
6.4 客户创建/更新后的销售归因
当客户在后续产生购买时,Dub 通过另外两个处理器完成销售归因:
checkout.session.completed(checkout-session-completed.ts/api/stripe/integration/webhook/checkout-session-completed.ts)):读取 Checkout Session 的 metadata 或已关联客户,找到该客户的 Lead 事件后记录 Sale;invoice.paid(invoice-paid.ts/api/stripe/integration/webhook/invoice-paid.ts)):先用stripeCustomerId反查 Dub 客户,查不到时再回连 Stripe 获取客户 metadata 中的dubCustomerExternalId,通过(projectConnectId, externalId)复合唯一键更新客户并补记stripeCustomerId,最后记录"订阅续费/发票支付"类型的 Sale 事件。
两个处理器都会在 Redis 中以trackSale:stripe:invoiceId:{invoiceId}为键做 7 天幂等去重,避免同一张发票被重复计入销售额。这也解释了为什么文档强调"当客户产生购买时,Dub 会自动将购买详情(发票金额、币种等)关联到原始点击事件"——正是这套 Webhook 链路在背后完成关联。
七、集成后的数据流全景
整个归因链路可以概括为:
- 用户点击 Dub 短链 → Dub 生成
dub_id点击事件并记录到 Tinybird; - 用户在你的站点完成注册/支付,请求头携带
dub_id; - 你的服务端创建/更新 Stripe 客户,把
dubCustomerExternalId(用户 ID)与dubClickId(dub_id)写入metadata; - Stripe 触发
customer.created/customer.updatedWebhook,Dub 在本地创建或更新Customer,并记录New customerLead 事件; - 客户后续支付触发
checkout.session.completed/invoice.paid,Dub 记录 Sale 事件(金额、币种、发票号),递增链接sales/saleAmount/conversions,必要时为合作伙伴生成佣金并发送 Postback。
八、最佳实践与注意事项
- 用户 ID 必须稳定唯一:
dubCustomerExternalId一旦确定就不要变更,它是 Dub 客户记录与 Stripe 客户长期绑定的主键;若 ID 变化,会导致历史归因断裂。 - 保持幂等:Webhook 对同一发票做了 Redis 去重,你侧重复调用
customers.update不会产生副作用,可以放心重试。 - 非 USD 币种自动换算:销售金额会统一换算为 USD 记录(convert-currency.ts),不需要你自行处理币种转换。
- 免费试用与折扣控制:工作区可在 Stripe 集成设置中配置
freeTrials(是否将订阅免费试用记录为 Lead)与discountCodeRestrictions(仅首次交易可使用折扣码),配置模式见 schema.ts。 - 订阅场景的元数据优先级:若同时使用了 Checkout Session 流程,Dub 会优先读取 Checkout Session 的
metadata,其次才是已连接客户对象上的metadata,两种方式互不冲突。
通过本文的接入方式,即使你的支付流程完全自定义、不经过 Stripe Checkout,也能让 Dub 精确地把每一笔销售回溯到最初的点击来源,为链接转化率分析、合作伙伴佣金结算和 Google Ads 转化回传提供准确的数据底座。
【免费下载链接】dubThe modern link attribution platform. Loved by world-class marketing teams like Framer, Perplexity, Superhuman, Twilio, Buffer and more.项目地址: https://gitcode.com/GitHub_Trending/du/dub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考