---
title: "Conversion Tracking"
description: "Dub implements an end-to-end conversion tracking and attribution engine that ingests edge interactions, records customer lead and sale funnels, and dispatches multi-channel commissions and downstre..."
last_updated: "2026-10-12T07:00:10.087014+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-pipeline/conversion-tracking"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/api/conversions/track-lead.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts)
- [apps/web/app/ee/api/track/application/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/application/route.ts)
- [apps/web/lib/api/conversions/track-sale.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts)
- [apps/web/app/ee/api/track/visit/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/visit/route.ts)
- [apps/web/app/ee/api/track/click/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/click/route.ts)
- [apps/web/app/ee/api/appsflyer/webhook/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/appsflyer/webhook/route.ts)
- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts)
- [apps/web/lib/api/customers/reattribute-customer.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts)
- [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts)
- [apps/web/lib/api/commissions/create-manual-commissions.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/commissions/create-manual-commissions.ts)
- [apps/web/app/ee/api/track/lead/client/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/lead/client/route.ts)
- [apps/web/app/ee/api/singular/webhook/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/singular/webhook/route.ts)
- [apps/web/app/ee/api/track/sale/client/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/sale/client/route.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/checkout-session-completed.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts)
- [apps/web/scripts/programs/backfill-reuse-commission.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/scripts/programs/backfill-reuse-commission.ts)
- [apps/web/app/ee/api/track/lead/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/lead/route.ts)
- [apps/web/app/ee/api/track/sale/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/sale/route.ts)
- [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts)
- [apps/web/lib/auth/track-dub-lead.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/track-dub-lead.ts)
- [apps/web/scripts/programs/3-import-customer-leads.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/scripts/programs/3-import-customer-leads.ts)
- [apps/web/lib/integrations/hubspot/track-lead.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/hubspot/track-lead.ts)
- [apps/web/lib/integrations/singular/track-lead.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/singular/track-lead.ts)
- [apps/web/lib/tinybird/record-click.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/tinybird/record-click.ts)
- [apps/web/lib/application-events/mark-application-event-submitted-network.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/application-events/mark-application-event-submitted-network.ts)
- [apps/web/app/ee/api/cron/streams/update-partner-stats/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-partner-stats/route.ts)
- [apps/web/lib/integrations/shopify/create-lead.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/create-lead.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts)
- [apps/web/lib/api/partners/sync-partner-links-stats.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/partners/sync-partner-links-stats.ts)
- [apps/web/lib/analytics/is-first-conversion.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/analytics/is-first-conversion.ts)
- [apps/web/lib/integrations/singular/track-sale.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/singular/track-sale.ts)
</details>

## Overview

Dub implements an end-to-end conversion tracking and attribution engine that ingests edge interactions, records customer lead and sale funnels, and dispatches multi-channel commissions and downstream webhooks. It resolves request identities, attributes events across organic links, discount codes, and merchant platforms like Stripe and Shopify, and synchronizes real-time partner performance metrics via Redis streams and analytical storage.
Sources: [apps/web/lib/api/conversions/track-lead.ts:112-162](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts#L112-L162)

## Edge ingestion for visits and clicks

### Visit and Click Tracking Endpoints

Dub exposes public edge tracking routes to ingest incoming client visits, record link interactions, and verify workspace hostnames against analytics configuration. These endpoints process incoming telemetry from client-side scripts and redirects, validating request parameters and coordinating with caching layers to deduplicate events and enrich click records.
Sources: [apps/web/app/ee/api/track/visit/route.ts:17-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/visit/route.ts#L17-L50)

The visit tracking route at `/api/track/visit` accepts POST requests containing a target domain, URL, and optional referrer. It parses the request body, checks if a click identifier is already cached in Redis via `recordClickCache`, and retrieves link details from edge storage if necessary. If no cached click identifier exists, a new 16-character identifier is generated using `nanoid`.
Sources: [apps/web/app/ee/api/track/visit/route.ts:19-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/visit/route.ts#L19-L54)

Similarly, the click tracking route at `/api/track/click` validates incoming payloads against `trackClickSchema`, enforcing domain normalization and presence checks for keys and URLs. It resolves request identity hashes to prevent duplicate logging within hourly windows and evaluates allowed workspace hostnames before recording click telemetry.
Sources: [apps/web/app/ee/api/track/click/route.ts:25-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/click/route.ts#L25-L33)

| Endpoint | Method | Request Schema | Core Action | Sources |
| :--- | :--- | :--- | :--- | :--- |
| `/api/track/visit` | `POST` | `domain`, `url`, `referrer` (optional) | Tracks client-side page views and logs visits | Sources: [apps/web/app/(ee)/api/track/visit/route.ts:17-105](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/visit/route.ts#L17-L105) |
| `/api/track/click` | `POST` | `domain`, `key`, `url`, `referrer` (optional) | Tracks link clicks, returns partner info and discounts | Sources: [apps/web/app/(ee)/api/track/click/route.ts:25-55](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/click/route.ts#L25-L55) |

> [!NOTE]
> Requests lacking a domain or URL in the visit route throw a `bad_request` error, while click tracking requests failing hostname verification return a `forbidden` error with configuration guidance.
> Sources: [apps/web/app/(ee)/api/track/visit/route.ts:22-27](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/visit/route.ts#L22-L27)

### Telemetry Recording and Deduplication

The underlying `recordClick` engine manages geolocation extraction, bot detection, and data ingestion into [Tinybird Analytics](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-pipeline/tinybird-analytics). It evaluates request headers to determine user agents, checks for bot traffic unless triggered via deeplinks, and respects `dub-no-track` headers or query parameters to bypass collection.
Sources: [apps/web/lib/tinybird/record-click.ts:53-78](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/tinybird/record-click.ts#L53-L78)

```mermaid
flowchart TD
    A[Incoming Request] --> B{Check dub-no-track}
    B -- Yes --> C[Ignore & Return Null]
    B -- No --> D{Trigger === deeplink?}
    D -- No --> E{Detect Bot}
    E -- Yes --> C
    E -- No --> F[Compute Identity Hash]
    D -- Yes --> F
    F --> G{Rate Limit Check}
    G -- Cached --> C
    G -- New --> H[Extract Geolocation & User Agent]
    H --> I[Dispatch to Tinybird & Upstash Streams]
    Sources: [apps/web/lib/tinybird/record-click.ts:59-200](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts#L59-L200)
```

> [!WARNING]
> When executing on Vercel, region codes are extracted from `x-vercel-ip-country-region` rather than standard geolocation helpers to ensure accurate sub-national geographic attribution.
> Sources: [apps/web/lib/tinybird/record-click.ts:110-116](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/tinybird/record-click.ts#L110-L116)

European Union visitors have their IP addresses scrubbed before being recorded in analytics tables to comply with data privacy requirements.
Sources: [apps/web/lib/tinybird/record-click.ts:122-137](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/tinybird/record-click.ts#L122-L137)

## Lead capture and attribution pipeline

### Server and Client Tracking Workflows

Lead conversion events are processed through unified server actions and API routes designed for both direct client integrations and authenticated server requests. The core lead processing function `trackLead` handles event intake across multiple execution modes: `wait`, `async`, and `deferred`. Client-side tracking operates via `/api/track/lead/client`, which requires a publishable key and validates incoming request hostnames against the workspace's configured `allowedHostnames`. Server-side tracking takes place via `/api/track/lead`, which authenticates requests through workspace membership cookies or API tokens while supporting legacy payload properties for backwards compatibility.
Sources: [apps/web/app/ee/api/track/lead/route.ts:10-36](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/lead/route.ts#L10-L36)

```typescript
const {
  clickId,
  eventName,
  eventQuantity,
  customerExternalId: newExternalId,
  externalId: oldExternalId, // deprecated (but we'll support it for backwards compatibility)
  customerId: oldCustomerId, // deprecated (but we'll support it for backwards compatibility)
  customerName,
  customerEmail,
  customerAvatar,
  mode,
  metadata,
} = trackLeadRequestSchema
  .extend({
    // we if clickId is undefined/nullish, we'll coerce into an empty string
    clickId: z.string().trim().nullish(),
    // add backwards compatibility
    customerExternalId: z.string().trim().nullish(),
    externalId: z.string().trim().nullish(),
    customerId: z.string().trim().nullish(),
  })
  .parse(body);
```
Sources: [apps/web/app/ee/api/track/lead/route.ts:14-35](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/lead/route.ts#L14-L35)

```typescript
if (!isDuplicateEvent) {
  // First, we need to find the click event
  const clickData = await getClickEvent({ clickId });

  // if there is no click data, throw an error
  if (!clickData) {
    throw new DubApiError({
      code: "not_found",
      message: `Click event not found for clickId: ${clickId}`,
    });
  }

```
Sources: [apps/web/lib/api/conversions/track-lead.ts:111-122](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts#L111-L122)

> [!WARNING]
> If a click identifier fails to resolve to a valid, enabled link owned by the target workspace, `trackLead` throws a `not_found` DubApiError and halts lead registration.
> Sources: [apps/web/lib/api/conversions/track-lead.ts:115-149](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts#L115-L149)

### Customer Resolution and Deduplication

Incoming leads are verified against Redis cache layers to prevent duplicate processing within a 7-week window by testing keys against `clickId`, `eventName`, and `customerExternalId`. When an event is new, the system queries click data and validates link ownership before provisioning or fetching customer records via `getOrCreateCustomer`. For authenticated Next.js users, `trackDubLead` extracts the `dub_id` cookie, submits a sign-up lead event with user metadata, and subsequently clears the tracking cookies.
Sources: [apps/web/lib/api/conversions/track-lead.ts:91-106](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts#L91-L106)

```typescript
export const trackDubLead = async (user: User) => {
  const cookieStore = await cookies();
  const clickId = cookieStore.get("dub_id")?.value;

  if (!clickId) {
    console.log("No dub_id cookie found, skipping lead tracking...");
    return;
  }

  // send the lead event to Dub
  await dub.track.lead({
    clickId,
    eventName: "Sign Up",
    customerExternalId: user.id,
    customerName: user.name,
    customerEmail: user.email,
    customerAvatar: user.image,
  });

```
Sources: [apps/web/lib/auth/track-dub-lead.ts:5-23](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/track-dub-lead.ts#L5-L23)

```typescript
if (!customer) {
  const { customer: existingOrNewCustomer } = await getOrCreateCustomer({
    where: {
      projectId_externalId: {
        projectId: workspace.id,
        externalId: customerExternalId,
      },
    },
    create: {
      id: finalCustomerId,
      name: finalCustomerName,
      email: customerEmail,
      avatar: finalCustomerAvatar,
      externalId: customerExternalId,
      projectId: workspace.id,
      projectConnectId: workspace.stripeConnectId,
      clickId: clickData.click_id,
      linkId: link.id,
      programId: link.programId,
      partnerId: link.partnerId,
      country: clickData.country,
      clickedAt: new Date(clickData.timestamp + "Z"),
    },
  });

  customer = existingOrNewCustomer;
}
```
Sources: [apps/web/lib/api/conversions/track-lead.ts:174-200](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts#L174-L200)

> [!TIP]
> In `wait` execution mode, lead event payloads are cached in Redis for 5 minutes under keys scoped to both customer ID and specific event names to bridge telemetry read-after-write delays from [Tinybird Analytics](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-pipeline/tinybird-analytics).
> Sources: [apps/web/lib/api/conversions/track-lead.ts:203-224](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-lead.ts#L203-L224)

### Third-Party Integrations and Webhook Delivery

Lead attribution extends to external merchant platforms like HubSpot, where `trackHubSpotLeadEvent` maps contact creations, property changes, and deal stage transitions into conversion events. When HubSpot objects meet criteria such as lifecycle stage progression or deal creation, deferred or asynchronous leads are recorded, and corresponding contact properties (`dub_link` and `dub_partner_email`) are updated back on HubSpot.
Sources: [apps/web/lib/integrations/hubspot/track-lead.ts:25-32](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/hubspot/track-lead.ts#L25-L32)

```typescript
if (objectTypeId === "0-1" && subscriptionType === "object.creation") {
  const contactInfo = await hubSpotApi.getContact(objectId);

  if (!contactInfo) {
    return `No contact info found for contact ${objectId}.`;
  }

  const { properties } = contactInfo;

  if (!properties.dub_id) {
    return `No dub_id found for contact ${objectId}.`;
  }

  const customerName =
    [properties.firstname, properties.lastname].filter(Boolean).join(" ") ||
    null;

  const trackLeadResult = await trackLead({
    clickId: properties.dub_id,
    eventName: "Sign up",
    customerEmail: properties.email,
    customerExternalId: properties.email,
    customerName,
    mode: "deferred",
    workspace,
    source: "hubspot",
    commissionSource: CommissionSource.hubspot,
  });

  if (trackLeadResult) {
    await updateHubSpotContact({
      contact: contactInfo,
      trackLeadResult,
      hubSpotApi,
    });
  }

  return `Deferred lead tracked for contact ${objectId}.`;
}
```
Sources: [apps/web/lib/integrations/hubspot/track-lead.ts:34-72](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/hubspot/track-lead.ts#L34-L72)

```typescript
export const updateHubSpotContact = async ({
  hubSpotApi,
  contact,
  trackLeadResult,
}: {
  hubSpotApi: HubSpotApi;
  contact: HubSpotContact;
  trackLeadResult: TrackLeadResponse;
}) => {
  if (contact.properties.dub_link && contact.properties.dub_partner_email) {
    console.log(
      `[HubSpot] Contact ${contact.id} already has dub_link and dub_partner_email. Skipping update.`,
    );
    return;
  }

  const properties: Record<string, string> = {};

  if (trackLeadResult.link?.partnerId) {
    const partner = await prisma.partner.findUniqueOrThrow({
      where: {
        id: trackLeadResult.link.partnerId,
      },
      select: {
        email: true,
      },
    });

    if (partner.email) {
      properties["dub_partner_email"] = partner.email;
    }
  }

  if (trackLeadResult.link?.shortLink) {
    properties["dub_link"] = trackLeadResult.link.shortLink;
  }

```
Sources: [apps/web/lib/integrations/hubspot/track-lead.ts:251-287](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/hubspot/track-lead.ts#L251-L287)

## Sale tracking and first conversion detection

### Sale Event Ingestion and Currency Normalization

Sale conversions are ingested via dedicated server-side endpoints and client-side publishable key routes. The primary server route `/api/track/sale` authenticates via workspace membership, parses requests using `trackSaleRequestSchema`, and handles legacy parameters with backwards compatibility for `externalId` and `customerId`. The client endpoint `/api/track/sale/client` verifies request hostnames against `allowedHostnames` stored on the workspace before passing payloads to `trackSale`.
Sources: [apps/web/app/ee/api/track/sale/route.ts:10-36](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/sale/route.ts#L10-L36)

```typescript
export const POST = withWorkspace(
  async ({ req, workspace }) => {
    const body = await parseRequestBody(req);

    let {
      customerExternalId: newExternalId,
      externalId: oldExternalId, // deprecated
      customerId: oldCustomerId, // deprecated
      customerName,
      customerEmail,
      customerAvatar,
      clickId,
      amount,
      currency,
      eventName,
      paymentProcessor,
      invoiceId,
      leadEventName,
      metadata,
    } = trackSaleRequestSchema
      .extend({
        // add backwards compatibility
        customerExternalId: z.string().nullish(),
        externalId: z.string().nullish(),
        customerId: z.string().nullish(),
      })
      .parse(body);

    const customerExternalId = newExternalId || oldExternalId || oldCustomerId;

    if (!customerExternalId) {
      throw new DubApiError({
        code: "bad_request",
        message: "customerExternalId is required",
      });
    }
```
Sources: [apps/web/app/ee/api/track/sale/route.ts:10-45](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/sale/route.ts#L10-L45)

```typescript
export const POST = withPublishableKey(
  async ({ req, workspace }) => {
    const body = await parseRequestBody(req);

    const allowRequest = verifyAnalyticsAllowedHostnames({
      allowedHostnames: (workspace?.allowedHostnames ?? []) as string[],
      req,
    });

    if (!allowRequest) {
      throw new DubApiError({
        code: "forbidden",
        message: `Request origin '${getHostnameFromRequest(req)}' is not included in the allowed hostnames for this workspace. Update your allowed hostnames here: https://app.dub.co/settings/tracking`,
      });
    }

    const {
      customerExternalId,
      customerName,
      customerEmail,
      customerAvatar,
      clickId,
      amount,
      currency,
      eventName,
      paymentProcessor,
      invoiceId,
      leadEventName,
      metadata,
    } = trackSaleRequestSchema.parse(body);

    if (!customerExternalId) {
      throw new DubApiError({
        code: "bad_request",
        message: "customerExternalId is required",
      });
    }
```
Sources: [apps/web/app/ee/api/track/sale/client/route.ts:14-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/sale/client/route.ts#L14-L50)

Within `_trackSale`, incoming transactions are evaluated and normalized before data recording. If the transaction amount is zero or negative, processing terminates early. Transactions denominated in non-USD currencies are converted to USD using current foreign exchange rates via `convertCurrency`.
Sources: [apps/web/lib/api/conversions/track-sale.ts:469-490](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L469-L490)

```typescript
// Skip if amount is 0 or less
if (amount <= 0) {
  return {
    eventName,
    customer: null,
    sale: null,
  };
}

// if currency is not USD, convert it to USD based on the current FX rate
// TODO: allow custom "defaultCurrency" on workspace table in the future
if (currency !== "usd") {
  const { currency: convertedCurrency, amount: convertedAmount } =
    await convertCurrency({
      currency,
      amount,
    });

  currency = convertedCurrency;
  amount = convertedAmount;
}

```
Sources: [apps/web/lib/api/conversions/track-sale.ts:493-514](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L493-L514)

> [!WARNING]
> If `leadEventData` cannot be resolved for a customer during sale ingestion, `_trackSale` throws a `not_found` error indicating that lead event data is missing.
> Sources: [apps/web/lib/api/conversions/track-sale.ts:486-491](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L486-L491)

### First Conversion Detection and Deduplication

First conversion detection determines whether a transaction represents an initial acquisition or a recurring customer purchase. The helper `isFirstConversion` evaluates customer metrics against the incoming `linkId`. A transaction is categorized as a first conversion if the customer has recorded zero previous sales, or if the customer's original referral link identifier differs from the current transaction link.
Sources: [apps/web/lib/analytics/is-first-conversion.ts:3-10](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/analytics/is-first-conversion.ts#L3-L10)

```typescript
export const isFirstConversion = ({
  customer,
  linkId,
}: {
  customer: Pick<Customer, "sales" | "linkId">;
  linkId?: string;
}) => {
  // if this is the first sale for the customer, it's a first conversion
  if (customer.sales === 0) {
    return true;
  }

  // if customer has sales, but the original referral link is not the same as the current link
  // it is most likely a first conversion
  if (customer.linkId !== linkId) {
    // TODO: fix edge case where customer was brought in by a different link, but then had recurring sales on the current link
    return true;
  }

  return false;
};
```
Sources: [apps/web/lib/analytics/is-first-conversion.ts:3-23](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/analytics/is-first-conversion.ts#L3-L23)

To prevent race conditions where concurrent requests attempt to record initial purchases simultaneously, first conversion claims are deduplicated using a Redis key with a 30-second expiry and an `nx: true` constraint.
Sources: [apps/web/lib/api/conversions/track-sale.ts:535-546](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L535-L546)

```typescript
let firstConversionFlag = isFirstConversion({
  customer,
  linkId: saleData.link_id,
});

// Deduplicate concurrent first sales for the same customer + link so only one
// request is counted as the first conversion.
if (firstConversionFlag) {
  const claim = await redis.set(
    `firstConversion:${customer.id}:${saleData.link_id}`,
    1,
    {
      ex: 30,
      nx: true,
    },
  );

  firstConversionFlag = claim !== null;
}

```
Sources: [apps/web/lib/api/conversions/track-sale.ts:528-547](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L528-L547)

> [!TIP]
> The Redis deduplication lock uses the key pattern `firstConversion:{customerId}:{linkId}` to ensure only a single concurrent request is counted as the first conversion for a given customer and link pairing.
> Sources: [apps/web/lib/api/conversions/track-sale.ts:536-543](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L536-L543)

### Commission Dispatch and Metrics Updates

Once sale data and conversion flags are established, asynchronous background processing updates link statistics and dispatches partner commissions. Prisma updates increment link metrics such as `sales` and `saleAmount`, while conditionally incrementing conversion counts and setting `lastConversionAt` when `firstConversionFlag` evaluates to true.
Sources: [apps/web/lib/api/conversions/track-sale.ts:548-570](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L548-L570)

```typescript
waitUntil(
  (async () => {
    // Update link conversions, sales, and saleAmount
    const link = await prisma.link.update({
      where: {
        id: saleData.link_id,
      },
      data: {
        ...(firstConversionFlag && {
          conversions: {
            increment: 1,
          },
          lastConversionAt: new Date(),
        }),
        sales: {
          increment: 1,
        },
        saleAmount: {
          increment: amount,
        },
      },
      include: includeTags,
    });
```
Sources: [apps/web/lib/api/conversions/track-sale.ts:548-570](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L548-L570)

If the associated link contains valid partner and program identifiers, `queuePartnerCommissionCreation` is invoked to calculate earnings through the [Commission Engine](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/billing-and-payouts/commission-engine). The payload bundles customer country, signup date, source provenance, product metadata, and the first conversion status.
Sources: [apps/web/lib/api/conversions/track-sale.ts:576-608](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L576-L608)

```typescript
if (link.programId && link.partnerId) {
  result = await queuePartnerCommissionCreation({
    event: "sale",
    programId: link.programId,
    partnerId: link.partnerId,
    linkId: link.id,
    customerId: customer.id,
    eventId: saleData.event_id,
    amount: saleData.amount,
    quantity: 1,
    invoiceId,
    currency,
    source: commissionSource,
    userId,
    ...(metadata != null && { metadata }),
    context: {
      customer: {
        country: customer.country,
        signupDate: customer.createdAt,
        source,
      },
      sale: {
        productId: metadata?.productId,
        amount: saleData.amount,
        ...(metadata != null && { metadata }),
      },
    },
    clickEvent: {
      url: saleData.url,
      referer: saleData.referer,
    },
    isFirstConversion: firstConversionFlag,
  });
```
Sources: [apps/web/lib/api/conversions/track-sale.ts:576-608](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/conversions/track-sale.ts#L576-L608)

## Promotion codes and merchant integrations

### Stripe Promotion Code Attribution Workflow

When a Stripe checkout session completes without a direct `dubCustomerExternalId` or matching customer record, Dub evaluates whether a Stripe promotion code identifier (`promotionCodeId`) was attached to the checkout session. The function `attributeViaPromotionCodeId` queries the connected Stripe account via `getPromotionCode` to resolve the promo code string. It then searches the database for a matching `DiscountCode` record within the workspace's default program.
Sources: [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:30-85](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L30-L85)

```typescript
export async function attributeViaPromotionCodeId({
  promotionCodeId,
  workspace,
  mode,
  customerDetails,
}: {
  promotionCodeId: string; // must be Stripe's promotion code ID `promo_xxx`, not the actual promo code
  workspace: Pick<
    Project,
    "id" | "defaultProgramId" | "stripeConnectId" | "webhookEnabled"
  >;
  mode: StripeMode;
  customerDetails: PromoCodeCustomerDetails;
}) {
  console.log(
    `Attributing via promotion code ID ${promotionCodeId} for customer ${customerDetails.stripeCustomerId} in workspace ${workspace.id}`,
  );

  const stripeAccountId = workspace.stripeConnectId!;
  const stripeCustomerId = customerDetails.stripeCustomerId;

  if (!workspace.defaultProgramId) {
    console.log(
      `Workspace with stripeConnectId ${stripeAccountId} has no default program, skipping...`,
    );
    return null;
  }

  // Find the promotion code for the promotion code id
  const promotionCode = await getPromotionCode({
    promotionCodeId,
    stripeAccountId,
    mode,
  });

  if (!promotionCode) {
    console.log(
      `Promotion code ${promotionCodeId} not found in connected account ${stripeAccountId}, skipping...`,
    );
    return null;
  }

  console.log(`Promotion code found: ${promotionCode.code}`);

  const discountCode = await prisma.discountCode.findUnique({
    where: {
      programId_code: {
        programId: workspace.defaultProgramId,
        code: promotionCode.code,
      },
    },
    include: {
      link: true,
    },
  });

  if (!discountCode) {
    console.log(
      `Couldn't find discount code "${promotionCode.code}" in program "${workspace.defaultProgramId}", skipping...`,
    );
    return null;
```
Sources: [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:30-90](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L30-L90)

Once the discount code and its associated link are identified and verified as active (not disabled), Dub simulates attribution by executing a fake click event using customer billing address geographic properties. It resolves or creates the customer record and records a lead event in Tinybird, while caching the lead payload in Redis with a five-minute expiration window to support immediate subsequent sale lookups.
Sources: [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:93-195](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L93-L195)

```typescript
const link = discountCode.link;
const linkId = link.id;
const customerAddress = customerDetails.address;
const customerCountry = customerAddress?.country?.toUpperCase();

// Record a fake click for this event
const clickEvent = await recordFakeClick({
  link,
  customer: {
    continent: customerCountry
      ? COUNTRIES_TO_CONTINENTS[customerCountry] ?? "Unknown"
      : "Unknown",
    country: customerCountry ?? "Unknown",
    region: customerAddress?.state ?? "Unknown",
  },
});

const { customer, created } = await getOrCreateCustomer({
  findMode: "first",
  where: {
    OR: [
      {
        stripeCustomerId,
      },
      {
        projectId: workspace.id,
        externalId: clickEvent.click_id,
      },
    ],
  },
  create: {
    id: createId({ prefix: "cus_" }),
    name:
      customerDetails.name || customerDetails.email || generateRandomName(),
    email: customerDetails.email,
    externalId: clickEvent.click_id,
    stripeCustomerId,
    linkId: clickEvent.link_id,
    clickId: clickEvent.click_id,
    clickedAt: new Date(clickEvent.timestamp + "Z"),
    country: customerAddress?.country,
    projectId: workspace.id,
    projectConnectId: workspace.stripeConnectId,
    programId: link.programId,
    partnerId: link.partnerId,
  },
});

// Concurrent webhook already created this customer — continue sale without re-recording lead
if (!created) {
  let existingLead = await redis.get<LeadEventTB>(`leadCache:${customer.id}`);

  if (!existingLead) {
    existingLead = await getLeadEvent({
      customerId: customer.id,
    });
  }

  if (!existingLead) {
    console.log(
      `Customer with stripeCustomerId ${stripeCustomerId} already exists but no lead event found, skipping promo code attribution...`,
    );
    return null;
  }

  return {
    linkId,
    customer,
    clickEvent,
    leadEvent: {
      ...existingLead,
      workspace_id: workspace.id,
    },
  };
}

// Prepare the payload for the lead event
const { timestamp, ...rest } = clickEvent;

const leadEvent = {
```
Sources: [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:100-195](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L100-L195)

> [!NOTE]
> If a concurrent webhook execution has already created the customer record, `attributeViaPromotionCodeId` bypasses duplicate lead recording and retrieves the existing lead event from Redis or Tinybird.
> Sources: [apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:148-174](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L148-L174)

### Shopify Discount Code Attribution

Shopify orders containing discount codes are handled by `attributeViaDiscountCode`, which extracts customer billing information and maps country codes to continents. A fake click event is generated via `recordFakeClick`, establishing geographic provenance for the order before a customer record is created with a `cus_` prefix.
Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:28-65](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L28-L65)

```typescript
export async function attributeViaDiscountCode({
  order,
  workspace,
  link,
}: {
  order: ShopifyOrder;
  workspace: Pick<WorkspaceProps, "id" | "defaultProgramId" | "webhookEnabled">;
  link: Link;
}) {
  const { customer: orderCustomer, billing_address: billingAddress } = order;

  const billingAddressCountry = billingAddress?.country_code?.toUpperCase();

  // Record a fake click for this event
  const clickEvent = await recordFakeClick({
    link,
    customer: {
      continent: billingAddressCountry
        ? COUNTRIES_TO_CONTINENTS[billingAddressCountry] ?? "Unknown"
        : "Unknown",
      country: billingAddressCountry ?? "Unknown",
      region: billingAddress?.province ?? "Unknown",
    },
  });

  const customerId = createId({ prefix: "cus_" });
  const clickId = clickEvent.click_id;

  // Create the customer before recording the fake click so a P2002 on
  // projectId_externalId never leaves an orphaned Tinybird click behind.
  const customer = await prisma.customer.create({
    data: {
      id: customerId,
      name: orderCustomer
        ? `${orderCustomer.first_name} ${orderCustomer.last_name}`.trim()
        : generateRandomName(),
      email: orderCustomer?.email,
      externalId: orderCustomer?.id?.toString() || customerId,
      linkId: link.id,
      clickId,
      clickedAt: new Date(),
      country: billingAddress?.country_code,
      projectId: workspace.id,
      programId: link.programId,
      partnerId: link.partnerId,
    },
  });
```
Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:19-65](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L19-L65)

After the customer record is established, a lead event named `"Checkout with discount code"` is recorded, link lead metrics are incremented, and partner commission queues, workflow executions, and Google Ads conversion uploads are triggered asynchronously.
Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:67-179](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L67-L179)

```typescript
// Prepare the payload for the lead event
const { timestamp, ...rest } = clickEvent;

const leadEvent = {
  ...rest,
  workspace_id: clickEvent.workspace_id || customer.projectId, // in case for some reason the click event doesn't have workspace_id
  event_id: nanoid(16),
  event_name: "Checkout with discount code",
  customer_id: customer.id,
  metadata: "",
};

await recordLead(leadEvent);

const linkUpdated = await prisma.link.update({
  where: {
    id: link.id,
  },
  data: {
    leads: {
      increment: 1,
    },
    lastLeadAt: new Date(),
  },
  include: includeTags,
});

let result:
  | Awaited<ReturnType<typeof queuePartnerCommissionCreation>>
  | undefined = undefined;

if (link.programId && link.partnerId) {
  result = await queuePartnerCommissionCreation({
    event: "lead",
    programId: link.programId,
    partnerId: link.partnerId,
    linkId: link.id,
    eventId: leadEvent.event_id,
    customerId: customer.id,
    quantity: 1,
    source: CommissionSource.shopify,
    context: {
      customer: {
        country: customer.country,
      },
    },
  });

  await Promise.allSettled([
    executeWorkflows({
      event: "leadRecorded",
      identity: {
        workspaceId: workspace.id,
        programId: link.programId,
        partnerId: link.partnerId,
      },
      metrics: {
        current: {
          leads: 1,
        },
      },
    }),

    syncPartnerLinksStats({
      partnerId: link.partnerId,
      programId: link.programId,
      eventType: "lead",
    }),
  ]);
}

await Promise.allSettled([
  sendWorkspaceWebhook({
    trigger: "lead.created",
    workspace,
    data: transformLeadEventData({
      ...clickEvent,
      eventName: "Checkout with discount code",
      link: linkUpdated,
      customer,
```
Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:67-185](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L67-L185)

> [!CAUTION]
> When creating Shopify customers during discount code attribution, the customer record is inserted before recording the fake click to ensure a unique constraint violation (`P2002` on `projectId_externalId`) never leaves an orphaned Tinybird click behind.
> Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:47-49](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L47-L49)

## Customer reattribution and historical reconciliation

### Customer Recreation and Reattribution Flow

When a customer needs to be reattributed across links, Dub executes `recreateCustomerForReattribution` inside a Prisma transaction. The original customer record is updated by appending `(old)` to its name and assigning a randomized external identifier prefix (`reattributed_`) while nullifying all partnership, link, click, and Stripe billing properties. Simultaneously, a new customer record is created with the target link, program, partner, and click identifiers.
Sources: [apps/web/lib/api/customers/reattribute-customer.ts:126-170](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L126-L170)

```typescript
export async function recreateCustomerForReattribution({
  customer,
  link,
  newCustomerId,
  newClickId,
}: {
  customer: Customer;
  link: Pick<Link, "id" | "programId" | "partnerId">;
  newCustomerId: string;
  newClickId: string;
}) {
  return await prisma.$transaction(async (tx) => {
    await tx.customer.update({
      where: {
        id: customer.id,
      },
      data: {
        name: customer.name ? `${customer.name} (old)` : undefined,
        externalId: `reattributed_${nanoid(32)}`,
        stripeCustomerId: null,
        linkId: null,
        clickId: null,
        clickedAt: null,
        programId: null,
        partnerId: null,
        sales: 0,
        saleAmount: 0,
        firstSaleAt: null,
        subscriptionCanceledAt: null,
      },
    });

    return await tx.customer.create({
      data: {
        id: newCustomerId,
        name: customer.name,
        email: customer.email,
        avatar: customer.avatar,
        externalId: customer.externalId,
        stripeCustomerId: customer.stripeCustomerId,
        clickedAt: customer.clickedAt,
        country: customer.country,
        sales: customer.sales,
        saleAmount: customer.saleAmount,
        firstSaleAt: customer.firstSaleAt,
        subscriptionCanceledAt: customer.subscriptionCanceledAt,
        projectId: customer.projectId,
        projectConnectId: customer.projectConnectId,
        createdAt: customer.createdAt,
        linkId: link.id,
        programId: link.programId,
        partnerId: link.partnerId,
        clickId: newClickId,
      },
    });
  });
}
```
Sources: [apps/web/lib/api/customers/reattribute-customer.ts:115-171](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L115-L171)

> [!NOTE]
> Reattribution stubs are identified strictly by checking if the external identifier starts with `reattributed_`, `dummy_`, or `retired_` while holding null values for `partnerId`, `linkId`, and `programId`.
> Sources: [apps/web/lib/api/customers/reattribute-customer.ts:77-96](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L77-L96)

Event reconciliation reads historical customer event telemetry from Tinybird via `getCustomerReattributeEvents`, enforcing an upper limit defined by `CUSTOMER_REATTRIBUTION_EVENTS_LIMIT` set to 500 events. If event counts breach this threshold, reattribution halts to prevent memory exhaustion during event processing plans.
Sources: [apps/web/lib/api/customers/reattribute-customer.ts:98-103](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L98-L103)

```typescript
export const CUSTOMER_REATTRIBUTION_EVENTS_LIMIT = 500;
const UNPAID_COMMISSION_STATUSES = ["pending", "hold", "processed"] as const;
const STATS_LOCK_TTL_SECONDS = 60 * 60 * 24;
```
Sources: [apps/web/lib/api/customers/reattribute-customer.ts:22-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L22-L24)

### Historical Backfill and Partition Splitting

Historical backfills for leads and clicks duplicate events under new customer identifiers while transferring existing event references. In migration scripts such as `backfill-reuse-commission.ts`, existing sale events are capped at a maximum limit of 5 entries per execution to prevent batch timeout failures.
Sources: [apps/web/scripts/programs/backfill-reuse-commission.ts:145-150](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/scripts/programs/backfill-reuse-commission.ts#L145-L150)

When importing customer leads in bulk, click payloads are grouped by calendar year to satisfy ClickHouse partition boundaries, since partition ingestion supports a maximum of 12 partitions per backfill batch.
Sources: [apps/web/scripts/programs/3-import-customer-leads.ts:157-169](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/scripts/programs/3-import-customer-leads.ts#L157-L169)

| Parameter / Constant | Value / Limit | Purpose |
| :--- | :--- | :--- |
| `CUSTOMER_REATTRIBUTION_EVENTS_LIMIT` | `500` | Maximum event retrieval ceiling for customer reattribution pipelines |
| `STATS_LOCK_TTL_SECONDS` | `86400` (24 hours) | Redis distributed lock expiration time for stat synchronization runs |
| Max Sale Events Backfill | `5` | Upper limit for existing sale events processed during reuse-commission backfills |
| ClickHouse Partitions | `12` | Maximum monthly partitions allowed for a single event backfill batch |

```typescript
export const CUSTOMER_REATTRIBUTION_EVENTS_LIMIT = 500;
const UNPAID_COMMISSION_STATUSES = ["pending", "hold", "processed"] as const;
const STATS_LOCK_TTL_SECONDS = 60 * 60 * 24;
```
Sources: [apps/web/lib/api/customers/reattribute-customer.ts:22-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L22-L24)

> [!CAUTION]
> Distributed stat updates during reattribution utilize Redis keys guarded by `runOnce`, which immediately deletes the lock key if the execution function throws an uncaught exception.
> Sources: [apps/web/lib/api/customers/reattribute-customer.ts:58-75](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/customers/reattribute-customer.ts#L58-L75)

## Partner metrics synchronization and batch processing

### Redis Streams and Asynchronous Activity Updates

Partner activity updates are published to an asynchronous Redis stream via `publishPartnerActivityEvent` in [Commission Engine](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/billing-and-payouts/commission-engine), capturing events with `programId`, `partnerId`, `eventType`, and `timestamp`. The cron route `GET /api/cron/streams/update-partner-stats` processes these streams in batches governed by `BATCH_SIZE` set to `6000`, running with `deleteAfterRead: true`.
Sources: [apps/web/app/ee/api/cron/streams/update-partner-stats/route.ts:15](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-partner-stats/route.ts#L15)

When a partner activity stream batch is processed, unique program-partner pairs are extracted to query grouped link metrics and commission totals in parallel via Prisma. Derived metrics such as net revenue, earnings per click, and consistency scores are computed before executing updates in sub-batches of size `50` against PlanetScale using raw SQL execution via `conn.execute`. If publishing to the Redis stream fails, `syncPartnerLinksStats` falls back directly to an atomic Prisma transaction executing `tx.link.aggregate` and updating `tx.programEnrollment`.
Sources: [apps/web/app/ee/api/cron/streams/update-partner-stats/route.ts:77-107](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-partner-stats/route.ts#L77-L107)

> [!NOTE]
> Consistency scores default to `50` and scale dynamically based on `daysSinceLastConversion`: `100` for 7 days or less, `85` for up to 30 days, `70` for up to 90 days, `55` for up to 180 days, and `40` for longer periods.
> Sources: [apps/web/app/(ee)/api/cron/streams/update-partner-stats/route.ts:210-229](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-partner-stats/route.ts#L210-L229)

### Backfill Processing and Application Tracking

The backfill cron route `apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts` records batch click telemetry, leads, zero-amount sale events, and paid commissions simultaneously while caching external ID and event name pairs in Redis using `redis.sadd`. Concurrently, short link click, lead, and sale counters are incremented using `link.updateMany` grouped by key occurrences.
Sources: [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts:295-329](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts#L295-L329)

```typescript
await Promise.all([
  // Record clicks
  fetch(
    `${process.env.TINYBIRD_API_URL}/v0/events?name=dub_click_events&wait=true`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TINYBIRD_API_KEY}`,
        "Content-Type": "application/x-ndjson",
      },
      body: dataArray.map((d) => JSON.stringify(d.clickData)).join("\n"),
    },
  ),

  // Record leads
  recordLeadWithTimestamp(dataArray.map((d) => d.leadEventData)),

  // Record commissions
  prisma.commission.createMany({
    data: dataArray.map((d) => d.commissionData),
    skipDuplicates: true,
  }),

  // Record sales
  recordSaleWithTimestamp(dataArray.map((d) => d.saleEventData)),

  // Cache the externalId:eventName pairs
  redis.sadd(
    CACHE_KEY,
    ...(dataArray.map(
      ({ payload: { externalId, eventName } }) =>
        `${externalId}:${eventName}`,
    ) as [string]),
  ),
]);
…
await Promise.all(
  Object.entries(groupedLinks).map(([count, linkKeys]) =>
    prisma.link.updateMany({
      where: {
        shortLink: {
          in: linkKeys.map((key) =>
            linkConstructorSimple({
              domain: DOMAIN,
              key,
            }),
          ),
        },
      },
      data: {
        clicks: {
          increment: parseInt(count),
        },
        leads: {
          increment: parseInt(count),
        },
        sales: {
          increment: parseInt(count),
        },
      },
    }),
  ),
```
Sources: [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts:295-329](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts#L295-L329), [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts:351-376](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts#L351-L376)

Application events are managed via `markApplicationEventSubmittedNetwork`, which validates network program cookies, updates application records within a transaction, creates customer profiles, and triggers `syncPartnerLinksStats` with an event type of `"lead"`.
Sources: [apps/web/lib/application-events/mark-application-event-submitted-network.ts:17-72](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/application-events/mark-application-event-submitted-network.ts#L17-L72)

```typescript
const cookieStore = await cookies();
const cookieName = getApplicationEventCookieName(NETWORK_PROGRAM_ID);
const applicationEventId = cookieStore.get(cookieName)?.value;

if (!applicationEventId) {
  console.log(
    `No application event ID found for network program + new partner ${partner.id}, skipping...`,
  );
  return;
}

const applicationEvent = await prisma.programApplicationEvent.findUnique({
  where: {
    id: applicationEventId,
    programId: NETWORK_PROGRAM_ID,
  },
});

if (!applicationEvent) {
  console.log(
    `No application event found for new partner ${partner.id}, skipping...`,
  );
  return;
}

if (applicationEvent.submittedAt) {
  console.log(
    `Application event already submitted for new partner ${partner.id}, skipping...`,
  );
  return;
}

await prisma.$transaction(async (tx) => {
  await tx.programApplicationEvent.update({
    where: {
      id: applicationEventId,
    },
    data: {
      // assuming application starts after 15-45 seconds of visiting the page
      startedAt: addSeconds(applicationEvent.visitedAt, randomInt(15, 45)),
      submittedAt: new Date(),
      partnerId: partner.id,
    },
  });

  if (applicationEvent.referredByPartnerId) {
    await tx.partner.update({
      where: {
        id: partner.id,
      },
      data: {
        referredByPartnerId: applicationEvent.referredByPartnerId,
      },
    });
  }
});
…
await prisma.link.update({
  where: {
    id: networkPartnerLink.id,
  },
  data: {
    clicks: {
      increment: 1,
    },
    lastClicked: applicationEvent.visitedAt,
    leads: {
      increment: 1,
    },
    lastLeadAt: new Date(),
  },
});
console.log(`Updated stats for link ${networkPartnerLink.id}`);

await syncPartnerLinksStats({
  partnerId: networkPartnerLink.partnerId!,
  programId: networkPartnerLink.programId!,
  eventType: "lead",
});
```
Sources: [apps/web/lib/application-events/mark-application-event-submitted-network.ts:17-72](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/application-events/mark-application-event-submitted-network.ts#L17-L72), [apps/web/lib/application-events/mark-application-event-submitted-network.ts:136-157](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/application-events/mark-application-event-submitted-network.ts#L136-L157)

## Related

- [Tinybird Analytics](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-pipeline/tinybird-analytics)
- [Commission Engine](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/billing-and-payouts/commission-engine)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
