> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dub

> 在 Dub 中跟踪销售转化事件，当客户通过 Dodo Payments 完成购买时。

## 介绍

[Dub](https://dub.co) 是一个强大的链接管理平台，帮助您创建、分享和跟踪短链接。通过将 Dodo Payments 与 Dub 集成，您可以在客户完成购买时自动跟踪销售转化事件，使您能够衡量营销活动和推荐计划的投资回报率。

当客户完成以下操作时，Dub 会记录一个 "销售" 事件：

* 完成一次性付款
* 订阅付费计划
* 进行定期订阅付款

<Info>
  此集成需要在您的链接上启用转化跟踪的 Dub 账户。
</Info>

<Tip>
  **联盟计划集成**：此集成还与 **Dub Partners** 无缝协作，以跟踪联盟推荐和佣金。使用 Dub 的转化跟踪将销售归因于您的联盟链接，并衡量合作伙伴的表现。了解有关设置联盟计划的更多信息，请参阅我们的 [联盟功能指南](/features/affiliates)。
</Tip>

## 工作原理

当用户点击您的 Dub 短链接时，Dub 通过存储在 cookie 中的唯一点击 ID (`dub_id`) 跟踪访客。要将销售归因于您的链接，您需要：

1. **在创建结账会话时捕获 Dub 的点击 ID**，从 `dub_id` cookie 中获取
2. **将点击 ID 存储**在您的支付元数据中，连同客户的外部 ID
3. **在支付成功时通过其 Track API 发送销售数据到 Dub**

这使得 Dub 能够将成功的销售与原始链接点击匹配，从而为您提供完整的转化归因。

## 先决条件

在设置此集成之前，请确保您拥有：

1. 一个 [Dub 账户](https://dub.co) 和一个工作区
2. 为您的链接启用转化跟踪
3. 您的 Dub API 密钥（可在您的 Dub 仪表板的设置 → API 密钥中找到）

## 开始使用

<Steps>
  <Step title="在 Dub 中启用转化跟踪">
    在您的 Dub 仪表板中，为您想要跟踪销售的链接启用转化跟踪。这允许 Dub 在客户完成购买时记录销售事件。

    <Info>
      了解有关在 [Dub 文档](https://dub.co/docs/conversions/quickstart) 中启用转化跟踪的更多信息。
    </Info>
  </Step>

  <Step title="获取您的 Dub API 密钥">
    导航到您的 [Dub 仪表板](https://app.dub.co) → 设置 → API 密钥，并创建一个具有 `conversions.write` 范围的新 API 密钥。

    <Warning>
      保持您的 API 密钥安全，切勿在客户端代码中暴露它。
    </Warning>
  </Step>

  <Step title="在结账中捕获点击 ID">
    在创建结账会话时，从 cookie 中捕获 Dub 点击 ID，并将其添加到您的支付元数据中。
  </Step>

  <Step title="通过 Webhook 发送销售数据">
    配置一个 webhook，在支付成功时将销售数据发送到 Dub 的 Track API。
  </Step>

  <Step title="完成！">
    销售转化事件现在将在您的 Dub 分析仪表板中显示，并完全归因于您的链接。
  </Step>
</Steps>

## 实施指南

### 第 1 步：将点击 ID 和客户 ID 添加到结账元数据

在创建结账会话时，从 cookie 中捕获 Dub 点击 ID，并将其与客户的外部 ID 一起包含在您的支付元数据中。

<CodeGroup>
  ```typescript Next.js theme={null}
  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: 'customer@example.com',
        name: 'John Doe',
      },
      product_cart: [{ product_id: productId, quantity: 1 }],
      metadata: {
        dub_click_id: dubClickId,           // Store Dub click ID
        dub_external_id: customerId,        // Store your customer's unique ID
      },
    });

    return payment;
  }
  ```

  ```javascript Express.js theme={null}
  const express = require('express');
  const DodoPayments = require('dodopayments').default;
  const router = express.Router();

  const client = new DodoPayments();

  router.post('/create-checkout', async (req, res) => {
    // Capture Dub click ID from cookie
    const dubClickId = req.cookies?.dub_id;

    const payment = await client.payments.create({
      billing: {
        city: 'New York',
        country: 'US',
        state: 'NY',
        street: '123 Main St',
        zipcode: '10001',
      },
      customer: {
        email: req.body.email,
        name: req.body.name,
      },
      product_cart: [{ product_id: req.body.productId, quantity: 1 }],
      metadata: {
        dub_click_id: dubClickId,           // Store Dub click ID
        dub_external_id: req.body.customerId, // Store your customer's unique ID
      },
    });

    res.json({ payment });
  });
  ```

  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI, Request
  from dodopayments import DodoPayments

  app = FastAPI()
  client = DodoPayments()

  @app.post("/create-checkout")
  async def create_checkout(request: Request):
      # Capture Dub click ID from cookie
      dub_click_id = request.cookies.get("dub_id")

      body = await request.json()

      payment = client.payments.create(
          billing={
              "city": "New York",
              "country": "US",
              "state": "NY",
              "street": "123 Main St",
              "zipcode": "10001",
          },
          customer={
              "email": body["email"],
              "name": body["name"],
          },
          product_cart=[{"product_id": body["product_id"], "quantity": 1}],
          metadata={
              "dub_click_id": dub_click_id,          # Store Dub click ID
              "dub_external_id": body["customer_id"], # Store your customer's unique ID
          },
      )

      return {"payment": payment}
  ```

  ```go Go theme={null}
  package main

  import (
      "context"

      "github.com/dodopayments/dodopayments-go"
  )

  func createCheckout(dubClickId, customerId, productId string) (*dodopayments.PaymentNewResponse, error) {
      client := dodopayments.NewClient()

      payment, err := client.Payments.New(context.Background(), dodopayments.PaymentNewParams{
          Billing: dodopayments.F(dodopayments.BillingAddressParam{
              City:    dodopayments.F("New York"),
              Country: dodopayments.F(dodopayments.CountryCodeUs),
              State:   dodopayments.F("NY"),
              Street:  dodopayments.F("123 Main St"),
              Zipcode: dodopayments.F("10001"),
          }),
          Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
              dodopayments.NewCustomerParam{
                  Email: dodopayments.F("customer@example.com"),
                  Name:  dodopayments.F("John Doe"),
              },
          ),
          ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{
              ProductID: dodopayments.F(productId),
              Quantity:  dodopayments.F(int64(1)),
          }}),
          Metadata: dodopayments.F(map[string]string{
              "dub_click_id":    dubClickId, // Store Dub click ID
              "dub_external_id": customerId, // Store your customer's unique ID
          }),
      })

      return payment, err
  }
  ```
</CodeGroup>

### 第 2 步：将销售数据发送到 Dub

配置一个 webhook 端点，在支付成功时将销售数据发送到 Dub 的 Track API。

<Steps>
  <Step title="Open the Webhook Section">
    在您的 Dodo Payments 仪表板中，导航到 **Webhooks → + Add Endpoint** 并展开集成下拉菜单。

    <Frame>
      <img src="https://mintcdn.com/dodopayments/T7_sl4YsBiM1QKIy/images/integrations/dub/add.png?fit=max&auto=format&n=T7_sl4YsBiM1QKIy&q=85&s=a8fc4c81c51c97ae499d4f8e1861aa88" alt="Add Endpoint and integrations dropdown" style={{ maxHeight: '500px', width: 'auto' }} width="872" height="542" data-path="images/integrations/dub/add.png" />
    </Frame>
  </Step>

  <Step title="选择 Dub">
    选择 **Dub** 集成卡片。
  </Step>

  <Step title="Enter API Key">
    在配置字段中提供您的 Dub API 密钥。

    <Frame>
      <img src="https://mintcdn.com/dodopayments/T7_sl4YsBiM1QKIy/images/integrations/dub/api-key.png?fit=max&auto=format&n=T7_sl4YsBiM1QKIy&q=85&s=7c753f9d273dae3f1276c308a39c182c" alt="Add API Key" style={{ maxHeight: '500px', width: 'auto' }} width="843" height="367" data-path="images/integrations/dub/api-key.png" />
    </Frame>
  </Step>

  <Step title="配置转换">
    编辑转换代码以格式化支付数据以适应 Dub 的 Track Sale API。
  </Step>

  <Step title="测试并创建">
    使用示例有效负载进行测试，然后单击 **创建** 以激活集成。
  </Step>
</Steps>

## 转换代码示例

### 基本销售跟踪

在支付成功时跟踪销售：

```javascript basic_sale.js icon="js" expandable theme={null}
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;
}
```

### 跟踪订阅销售

跟踪初始订阅和定期付款：

```javascript subscription_sale.js icon="js" expandable theme={null}
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 以进行准确的收入跟踪：

```javascript sale_without_tax.js icon="js" expandable theme={null}
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;
}
```

### 使用自定义事件名称跟踪销售

使用自定义事件名称对不同类型的销售进行分类：

```javascript custom_events.js icon="js" expandable theme={null}
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：

<CodeGroup>
  ```typescript Next.js (Server Action) theme={null}
  '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,
    });
  }
  ```

  ```javascript Node.js theme={null}
  const { Dub } = require('dub');

  const dub = new Dub();

  async function trackSale(payment) {
    if (payment.metadata?.dub_click_id) {
      await dub.track.sale({
        clickId: payment.metadata.dub_click_id,
        externalId: payment.metadata.dub_external_id,
        amount: payment.total_amount,
        currency: payment.currency,
        paymentProcessor: 'dodo',
        invoiceId: payment.payment_id,
      });
    }
  }
  ```
</CodeGroup>

## 最佳实践

<Tip>
  **尽早捕获点击 ID**：在结账流程中尽早存储 Dub 点击 ID，以确保准确的归因，即使用户导航离开并稍后返回。
</Tip>

* **始终在元数据中包含点击 ID**：没有点击 ID，Dub 无法将收入归因于您的链接
* **一致使用外部 ID**：传递您在系统中使用的相同客户 ID，以便进行准确的客户级分析
* **优雅处理自然流量**：在没有点击 ID 时设置 `webhook.cancel = true` 以避免不必要的 API 调用
* **使用示例支付进行测试**：在上线之前验证集成是否正常工作
* **监控您的 Dub 仪表板**：检查销售是否正确显示并具有适当的归因

## 重要说明

<Note>
  * **金额格式**：Dub 期望金额以美分为单位（例如，\$10.00 = 1000）
  * **货币**：使用 ISO 4217 货币代码（USD、EUR、GBP 等）
  * **免费试用**：\$0 的支付不会被跟踪为销售
  * **退款**：如有需要，考虑单独跟踪退款以进行准确的收入报告
</Note>

## 故障排除

<AccordionGroup>
  <Accordion title="销售未出现在 Dub 中">
    * 验证您的 Dub API 密钥是否正确，并具有 `conversions.write` 范围
    * 检查 `dub_click_id` 是否被捕获并存储在支付元数据中
    * 确保 webhook 转换正确格式化有效负载
    * 验证 webhook 是否在 `payment.succeeded` 事件上触发
    * 确认为您的 Dub 链接启用了转化跟踪
  </Accordion>

  <Accordion title="收入归因未生效">
    * 确认用户在结账前点击了您的 Dub 短链接
    * 验证 `dub_id` cookie 是否在您的域上正确设置
    * 检查点击 ID 是否在结账创建和支付完成之间匹配
    * 确保您在创建结账会话之前捕获点击 ID
  </Accordion>

  <Accordion title="转换错误">
    * 验证 JSON 结构是否符合 Dub 的 Track Sale API 格式
    * 检查所有必需字段 (`clickId`, `externalId`, `amount`) 是否存在
    * 确保金额以美分为单位（整数，而不是小数）
    * 验证 API 端点 URL 是否正确：`https://api.dub.co/track/sale`
    * 使用示例 webhook 有效负载测试转换
  </Accordion>

  <Accordion title="重复销售被跟踪">
    * 确保您仅在 `payment.succeeded` 事件上进行跟踪，而不是 `payment.processing`
    * 为每笔销售使用唯一的 `invoiceId` 值
    * 对于订阅，在续订时附加时间戳或计费周期以防止重复
  </Accordion>
</AccordionGroup>

## 其他资源

<CardGroup cols={2}>
  <Card title="Dub 转化文档" icon="book" href="https://dub.co/docs/conversions/quickstart">
    了解有关 Dub 的转化跟踪和分析功能的更多信息。
  </Card>

  <Card title="Dub Track Sale API" icon="code" href="https://dub.co/docs/api-reference/track/track-sale">
    查看 Dub 的 Track Sale 端点的完整 API 参考。
  </Card>

  <Card title="Dub 仪表板" icon="chart-line" href="https://app.dub.co">
    访问您的 Dub 仪表板以查看转化分析和归因数据。
  </Card>

  <Card title="Webhook 事件指南" icon="webhook" href="/developer-resources/webhooks/intents/webhook-events-guide">
    了解所有可用的 Dodo Payments webhook 事件。
  </Card>
</CardGroup>

<Info>
  需要帮助吗？请联系 Dodo Payments 支持，邮箱 [support@dodopayments.com](mailto:support@dodopayments.com)，以获取集成的帮助。
</Info>
