---
title: "Conversion and Event Tracking"
description: "Conversion and event tracking form the core infrastructure for capturing visitor interactions, link performance, and downstream attribution across the system. This subsystem solves the complexity o..."
last_updated: "2026-10-05T05:07:35.222511+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/conversion-and-event-tracking"
---

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

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

- [apps/web/app/ee/api/track/lead/client/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/client/route.ts)
- [apps/web/app/ee/api/track/application/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts)
- [apps/web/app/ee/api/track/visit/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts)
- [apps/web/app/ee/api/track/click/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts)
- [apps/web/app/ee/api/appsflyer/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts)
- [apps/web/lib/integrations/google-ads/api.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts)
- [apps/web/app/ee/api/track/sale/client/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/client/route.ts)
- [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts)
- [apps/web/app/ee/api/track/lead/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/route.ts)
- [apps/web/lib/api/conversions/track-sale.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts)
- [apps/web/app/ee/api/track/open/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts)
- [apps/web/lib/api/conversions/track-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-lead.ts)
- [apps/web/app/ee/api/track/sale/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/route.ts)
- [apps/web/app/ee/api/singular/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts)
- [apps/web/app/ee/api/google-ads/conversion-actions/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/google-ads/conversion-actions/route.ts)
- [apps/web/app/ee/api/shopify/pixel/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts)
- [apps/web/lib/auth/track-dub-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/track-dub-lead.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/checkout-session-completed.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts)
- [apps/web/lib/api/commissions/create-manual-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/create-manual-commissions.ts)
- [apps/web/app/ee/api/google-ads/upload-conversion/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/google-ads/upload-conversion/route.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/utils/sync-customer.ts)
- [apps/web/lib/integrations/google-ads/upload-conversion.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/upload-conversion.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts)
- [apps/web/scripts/dev/simulate-shopify-conversion.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/simulate-shopify-conversion.ts)
- [apps/web/lib/api/customers/reattribute-customer.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts)
- [apps/web/lib/integrations/hubspot/track-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/hubspot/track-lead.ts)
- [apps/web/lib/openapi/track/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/track/index.ts)
- [apps/web/lib/integrations/singular/track-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/singular/track-lead.ts)
- [apps/web/lib/tinybird/record-click.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts)
- [apps/web/lib/analytics/get-customer-events.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/get-customer-events.ts)
</details>

## Overview

Conversion and event tracking form the core infrastructure for capturing visitor interactions, link performance, and downstream attribution across the system. This subsystem solves the complexity of bridging client-side click handling with server-side business intelligence by offering secure client ingestion endpoints, edge caching routers, robust bot filtering pipelines, and server-side attribution engines for both leads and sales. Key design decisions include utilizing Redis for low-latency deduplication and caching, Tinybird for high-throughput analytical event logging, and integrated webhooks or direct API handlers to support external processors like Stripe, Shopify, AppsFlyer, Singular, and Google Ads. By orchestrating automated workflows, partner commission triggers, and historical event replay pipelines, the tracking layer ensures reliable attribution and unified analytics across the entire ecosystem.

Sources: [apps/web/app/ee/api/track/lead/client/route.ts:1-60](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/client/route.ts#L1-L60), [apps/web/app/ee/api/track/application/route.ts:41-117](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L41-L117), [apps/web/app/ee/api/track/visit/route.ts:17-117](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts#L17-L117), [apps/web/app/ee/api/track/click/route.ts:57-183](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts#L57-L183), [apps/web/app/ee/api/appsflyer/webhook/route.ts:27-177](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L27-L177), [apps/web/lib/integrations/google-ads/api.ts:443-503](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L443-L503), [apps/web/app/ee/api/track/sale/client/route.ts:13-73](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/client/route.ts#L13-L73), [apps/web/lib/api/conversions/track-sale.ts:363-531](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts#L363-L531), [apps/web/app/ee/api/track/open/route.ts:23-202](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L23-L202), [apps/web/lib/api/conversions/track-lead.ts:112-231](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-lead.ts#L112-L231), [apps/web/app/ee/api/stripe/integration/webhook/checkout-session-completed.ts:153-324](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts#L153-L324), [apps/web/lib/tinybird/record-click.ts:22-236](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts#L22-L236)

## Client-Side Ingestion and Edge Routing

### Client-Side Ingestion and Edge Routing

Client-side ingestion relies on public Next.js API routes under `apps/web/app/(ee)/api/track/` to accept tracking payloads for visits, clicks, deep link opens, and client-side conversions (leads and sales). Each route handles cross-origin requests by returning `COMMON_CORS_HEADERS` and responding to preflight `OPTIONS` requests with status `204`.

Sources: [apps/web/app/ee/api/track/lead/client/route.ts:6-67](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/client/route.ts#L6-L67), [apps/web/app/ee/api/track/visit/route.ts:2-124](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts#L2-L124), [apps/web/app/ee/api/track/click/route.ts:6-190](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts#L6-L190), [apps/web/app/ee/api/track/sale/client/route.ts:6-80](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/client/route.ts#L6-L80), [apps/web/app/ee/api/track/open/route.ts:2-209](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L2-L209)

### Endpoint Routing and Schema Validation

Incoming requests undergo parsing and validation using Zod schemas before hitting downstream attribution wrappers or database resolvers. Click tracking validates domains via `getDomainWithoutWWW` and requires a link key, optionally accepting custom URLs and referrers. Visit and open endpoints resolve paths by extracting pathname segments, defaulting root paths to `_root`.

Sources: [apps/web/app/ee/api/track/click/route.ts:25-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts#L25-L62), [apps/web/app/ee/api/track/visit/route.ts:20-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts#L20-L34), [apps/web/app/ee/api/track/open/route.ts:31-79](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L31-L79)

| Endpoint Route | HTTP Method | Validation Schema | Required Plan / Auth |
| :--- | :--- | :--- | :--- |
| `/api/track/click` | POST | `trackClickSchema` | Public (`withAxiom`) |
| `/api/track/visit` | POST | Request body JSON (`domain`, `url`) | Public (`withAxiom`) |
| `/api/track/open` | POST | `trackOpenRequestSchema` | Public (`withAxiom`) |
| `/api/track/lead/client` | POST | `trackLeadRequestSchema` | `["business", "advanced", "enterprise"]` (`withPublishableKey`) |
| `/api/track/sale/client` | POST | `trackSaleRequestSchema` | `["business", "advanced", "enterprise"]` (`withPublishableKey`) |

Sources: [apps/web/app/ee/api/track/lead/client/route.ts:9-60](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/client/route.ts#L9-L60), [apps/web/app/ee/api/track/visit/route.ts:18-27](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts#L18-L27), [apps/web/app/ee/api/track/click/route.ts:25-61](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts#L25-L61), [apps/web/app/ee/api/track/sale/client/route.ts:9-73](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/client/route.ts#L9-L73), [apps/web/app/ee/api/track/open/route.ts:15-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L15-L33)

### Caching and Edge Resolution Architecture

To minimize database load, client endpoints parallelize lookups across Redis global caches using `redisGlobalWithTimeout`. For click tracking, the pipeline queries `recordClickCache` and `linkCache` simultaneously to check for existing click IDs and pre-fetched link properties.

```mermaid
sequenceDiagram
  autonumber
  participant Client
  participant NextRoute as Next.js API Route
  participant Redis as Redis Global Cache
  participant EdgeDB as PlanetScale Edge DB
  participant TB as Tinybird & Streams

  Client->>NextRoute: POST /api/track/click
  NextRoute->>Redis: mget(recordClickCache, linkCache)
  alt Click ID or Link Cached
    Redis-->>NextRoute: Return cached values
  else Cache Miss
    NextRoute->>EdgeDB: getLinkWithPartner() / getLinkViaEdge()
    EdgeDB-->>NextRoute: Link and partner props
    NextRoute->>Redis: linkCache.set() (via waitUntil)
  end
  NextRoute->>NextRoute: Verify allowed hostnames
  alt New Unique Click
    NextRoute->>TB: recordClick() -> ingest event & streams
  end
  NextRoute-->>Client: Return JSON response with clickId
```

Sources: [apps/web/app/ee/api/track/visit/route.ts:38-71](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts#L38-L71), [apps/web/app/ee/api/track/click/route.ts:66-98](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts#L66-L98), [apps/web/app/ee/api/track/open/route.ts:80-109](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L80-L109), [apps/web/lib/tinybird/record-click.ts:169-233](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts#L169-L233)

> [!NOTE]
> When a click ID is newly generated or recorded via `recordClick`, `shouldCacheClickId` instructs Redis to cache the full `clickData` object under `clickIdCache:${clickId}` with a 5-minute expiration (`ex: 60 * 5`) to bridge ingestion lag before events appear in Tinybird.

Sources: [apps/web/lib/tinybird/record-click.ts:163-167](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts#L163-L167)

### Hostname Verification and Security Controls

Client-side lead and sale tracking endpoints use `withPublishableKey` middleware alongside `verifyAnalyticsAllowedHostnames` to ensure requests originate from permitted domains configured on the workspace. If an unauthorized origin calls the endpoint, a `DubApiError` with code `forbidden` is thrown, referencing the settings dashboard URL.

> [!WARNING]
> Requests containing the `dub-no-track` HTTP header or `dub-no-track` query parameter are immediately dropped, returning `null` before bot detection or analytics recording executes.

Sources: [apps/web/app/ee/api/track/lead/client/route.ts:14-28](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/client/route.ts#L14-L28), [apps/web/app/ee/api/track/sale/client/route.ts:14-28](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/client/route.ts#L14-L28), [apps/web/lib/tinybird/record-click.ts:57-62](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts#L57-L62)

## Application Event Ingestion Pipeline

### Overview

The application event ingestion pipeline processes marketplace and program lifecycle tracking events such as visits and starts. The entry point handles incoming requests via `POST /api/track/application`, wrapping execution with Axiom logging and CORS headers.

Sources: [apps/web/app/ee/api/track/application/route.ts:41-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L41-L43), [apps/web/app/ee/api/track/application/route.ts:119-124](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L119-L124)

### Bot Filtering and Rate Limiting Execution

The pipeline executes initial validation and security checks in a strict call order before parsing request payloads.

1. `detectBot(req)` — Evaluates user agent and request characteristics against bot signatures; if a bot is detected, an immediate `202` response with `{ ok: true }` is returned.
2. `getIP()` — Resolves the client IP address for rate-limiting identification.
3. `assertRateLimit()` — Validates the IP against the `RATELIMIT_POLICIES.trackApplication` policy via Upstash.
4. `trackApplicationEventSchema.parse()` — Parses and validates the request body for `eventName`, `url`, and `referrer`.

Sources: [apps/web/app/ee/api/track/application/route.ts:44-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L44-L59)

> [!WARNING]
> Bot requests bypass database queries and rate-limit checks entirely, returning an HTTP `202` status code to prevent bot traffic from polluting marketplace application analytics.

Sources: [apps/web/app/ee/api/track/application/route.ts:44-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L44-L49)

### Program Resolution and Event Dispatch

After payload validation, the pipeline identifies the target program and dispatches lifecycle events based on the requested event name.

```mermaid
sequenceDiagram
  autonumber
  participant Client
  participant API as POST /api/track/application
  participant DB as Prisma Database
  participant Redis as Redis Cache

  Client->>API: POST tracking payload (eventName, url, referrer)
  API->>API: detectBot() & assertRateLimit()
  API->>API: identityProgramSlug(url)
  alt Program Slug is Network Program
    API->>API: Assign NETWORK_PROGRAM_ID
  else Custom Program Slug
    API->>DB: prisma.program.findUnique(slug)
    DB-->>API: Program Record
  end
  alt eventName === "visit"
    API->>API: trackVisitEvent()
    API->>Redis: Check visit cookie & enrollment
    API->>DB: prisma.programApplicationEvent.create()
  else eventName === "start"
    API->>API: trackStartEvent()
  end
  API-->>Client: 202 Accepted { ok: true }
```

Sources: [apps/web/app/ee/api/track/application/route.ts:44-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L44-L113)

| Step Name | Function / Method | Target / Entity | Purpose |
| :--- | :--- | :--- | :--- |
| Bot Detection | `detectBot(req)` | NextRequest | Filters automated scrapers and bots |
| Rate Limiting | `assertRateLimit()` | Upstash Redis | Enforces `RATELIMIT_POLICIES.trackApplication` by IP |
| Program Identification | `identityProgramSlug(url)` | URL string | Extracts program slug and marketplace flag |
| Program Lookup | `prisma.program.findUnique()` | Database | Resolves program ID from slug or network constant |
| Event Dispatch | `trackVisitEvent()` / `trackStartEvent()` | Database & Cookies | Records marketplace application lifecycle events |

Sources: [apps/web/app/ee/api/track/application/route.ts:44-108](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts#L44-L108)

## Server-Side Lead Attribution Engine

### Overview

The server-side lead attribution engine processes direct API requests, client SDK telemetry, and internal authentication flows to attribute lead conversion events to click IDs, resolve customer profiles, and trigger partner commissions and webhooks. The core HTTP endpoint is mounted at `POST /api/track/lead`, requiring authenticated workspace membership with business, advanced, or enterprise plans.

Sources: [apps/web/app/ee/api/track/lead/route.ts:9-11](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/route.ts#L9-L11), [apps/web/app/ee/api/track/lead/route.ts:61-65](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/route.ts#L61-L65)

### Lead Request Validation and Backwards Compatibility

Incoming payloads are parsed against a Zod validation schema that normalizes fields and handles legacy parameter naming for backwards compatibility.

| Parameter | Type | Required / Optional | Purpose |
| :--- | :--- | :--- | :--- |
| `clickId` | string | Optional (coerced to empty string) | Associated visitor click identifier |
| `eventName` | string | Optional | Name of the conversion event |
| `eventQuantity` | number | Optional | Multiplier quantity for the conversion event |
| `customerExternalId` | string | Optional (New) | Unique external identifier for the customer |
| `externalId` | string | Optional (Deprecated) | Legacy fallback field for customer external identifier |
| `customerId` | string | Optional (Deprecated) | Legacy fallback field for customer identifier |
| `customerName` | string | Optional | Human-readable name of the customer |
| `customerEmail` | string | Optional | Email address of the customer |
| `customerAvatar` | string | Optional | URL to the customer's avatar image |
| `mode` | string | Optional | Processing mode (`wait`, `deferred`, or standard) |
| `metadata` | object | Optional | Custom key-value metadata associated with the lead |

Sources: [apps/web/app/ee/api/track/lead/route.ts:14-35](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/route.ts#L14-L35)

> [!WARNING]
> The resolver checks `customerExternalId`, `externalId`, and `customerId` in sequential fallback order (`newExternalId || oldExternalId || oldCustomerId`). If all three resolve to a nullish value, a `bad_request` `DubApiError` is thrown immediately.

Sources: [apps/web/app/ee/api/track/lead/route.ts:37-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/route.ts#L37-L44)

### Lead Attribution Call Chain and Processing Execution

The lead attribution engine executes through a sequential pipeline from request handling to asynchronous post-processing.

```mermaid
sequenceDiagram
  autonumber
  participant Client
  participant API as POST /api/track/lead
  participant Track as trackLead()
  participant Redis as Upstash Redis
  participant Tinybird as Tinybird Analytics
  participant DB as Prisma Database

  Client->>API: POST /api/track/lead (clickId, customerExternalId, eventName)
  API->>API: withWorkspace() & parseRequestBody()
  API->>Track: trackLead(payload, workspace)
  Track->>Redis: Redis setnx deduplication check (leadCache / 1 week)
  alt Duplicate Event Detected
    Track-->>API: Return cached or duplicate status
  else New Event
    Track->>Tinybird: getClickEvent({ clickId })
    Tinybird-->>Track: clickData
    Track->>DB: prisma.link.findUnique({ id: clickData.link_id })
    DB-->>Track: link record (verify ownership & status)
    Track->>DB: getOrCreateCustomer() resolve/create customer
    alt mode === "wait"
      Track->>Redis: Cache lead payload for 5 minutes
    end
    Track->>Tinybird: recordLead(leadEventPayload) (unless deferred)
    Track->>DB: prisma.link.update() & prisma.project.update() usage
    Track->>Track: queuePartnerCommissionCreation() & sendWorkspaceWebhook()
  end
  API-->>Client: JSON response
```

Sources: [apps/web/app/ee/api/track/lead/route.ts:12-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/lead/route.ts#L12-L59), [apps/web/lib/api/conversions/track-lead.ts:112-321](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-lead.ts#L112-L321)

### Internal Authentication Lead Tracking

Dub tracks authentication-triggered sign-up leads internally via `trackDubLead()`. This utility retrieves the visitor tracking cookie (`dub_id`), invokes the underlying SDK tracking method with a `"Sign Up"` event name, and subsequently purges tracking cookies.

```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,
  });

  // delete the cookies
  cookieStore.delete("dub_id");
  cookieStore.delete("dub_partner_data");
};
```

Sources: [apps/web/lib/auth/track-dub-lead.ts:5-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/track-dub-lead.ts#L5-L27)

> [!NOTE]
> OpenAPI path definitions register `/track/lead` under the `trackPaths` dictionary mapping directly to the POST handler implementation for automated API documentation generation.

Sources: [apps/web/lib/openapi/track/index.ts:6-9](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/track/index.ts#L6-L9)

## Server-Side Sale Attribution Engine

### Overview

The sale attribution engine handles conversion processing for revenue events, currency normalization, first-conversion detection, and partner commission triggers. Incoming sale requests are processed via the protected workspace endpoint at `POST /api/track/sale`, which requires workspace authentication and specific plan tiers (`business`, `advanced`, or `enterprise`) with `owner` or `member` roles.

Sources: [apps/web/app/ee/api/track/sale/route.ts:9-69](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/route.ts#L9-L69)

### Sale Processing and Currency Normalization Call Chain

When a sale request hits the API route, parameters are validated using Zod schemas supporting backward compatibility aliases (`customerExternalId`, `externalId`, and `customerId`). The execution pipeline flows through core validation, currency conversion, and event recording.

```mermaid
sequenceDiagram
  autonumber
  participant Client
  participant API as POST /api/track/sale
  participant Track as trackSale()
  participant Currency as convertCurrency()
  participant DB as Prisma Database

  Client->>API: POST /api/track/sale (amount, currency, customerExternalId)
  API->>API: withWorkspace() & parseRequestBody()
  API->>Track: trackSale(payload, workspace)
  Track->>Track: Resolve or create customer & validate lead event data
  alt amount <= 0
    Track-->>API: Return null sale response
  else amount > 0
    alt currency !== "usd"
      Track->>Currency: convertCurrency({ currency, amount })
      Currency-->>Track: converted currency & amount
    end
    Track->>DB: Check first conversion status (isFirstConversion)
    Track->>DB: Record sale event & trigger partner commissions
  end
  API-->>Client: JSON response
```

Sources: [apps/web/app/ee/api/track/sale/route.ts:10-63](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/route.ts#L10-L63), [apps/web/lib/api/conversions/track-sale.ts:469-531](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts#L469-L531)

### Sale Request Parameters Reference

The sale ingestion payload accepts several standard and financial attributes to record and attribute revenue events accurately.

| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `customerExternalId` | string | Yes (or fallback) | Unique external identifier for the customer |
| `externalId` | string | Optional | Deprecated fallback for `customerExternalId` |
| `customerId` | string | Optional | Deprecated fallback for `customerExternalId` |
| `amount` | number | Yes | Monetary value of the sale transaction |
| `currency` | string | Optional | Currency code for the transaction (defaults to `"usd"`) |
| `eventName` | string | Optional | Name of the sale event |
| `paymentProcessor` | string | Optional | Payment gateway processor name (e.g. `"stripe"`, `"custom"`) |
| `invoiceId` | string | Optional | External invoice reference ID |
| `leadEventName` | string | Optional | Associated lead event name |
| `metadata` | object | Optional | Custom key-value metadata associated with the sale |

Sources: [apps/web/app/ee/api/track/sale/route.ts:14-36](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/route.ts#L14-L36), [apps/web/lib/api/conversions/track-sale.ts:469-485](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts#L469-L485)

> [!WARNING]
> If the transaction `amount` is less than or equal to `0`, the sale tracking function immediately bypasses event recording and returns a null sale object, preventing zero-value or negative revenue pollution in analytics.

Sources: [apps/web/app/ee/api/track/sale/route.ts:41-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/sale/route.ts#L41-L45), [apps/web/lib/api/conversions/track-sale.ts:493-500](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts#L493-L500)

### Currency Conversion and Commission Integration

Non-USD currencies are automatically normalized prior to sale persistence. When `currency !== "usd"`, the engine invokes `convertCurrency` to compute the converted amount and standardized currency code. Commission generation sources default to `CommissionSource.api` when manual or automated sales are processed through tracking routes.

```typescript
  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:479-513](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts#L479-L513)

> [!NOTE]
> OpenAPI path definitions map `/track/sale` within the `trackPaths` schema dictionary directly to the POST sale tracking endpoint handler.

Sources: [apps/web/lib/openapi/track/index.ts:6-12](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/track/index.ts#L6-L12)

## Integration Ingestion and Webhooks

### Overview

Conversion attribution is synchronized from external services via dedicated ingestion webhooks and client-side tracking pixels. The platform verifies requests against provider IP ranges or security signatures before processing conversions from AppsFlyer, Singular, Shopify, and Stripe.

Sources: [apps/web/app/ee/api/appsflyer/webhook/route.ts:26-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L26-L49), [apps/web/app/ee/api/singular/webhook/route.ts:37-52](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L37-L52), [apps/web/app/ee/api/shopify/pixel/route.ts:20-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L20-L82)

### AppsFlyer and Singular Postback Processing

Both AppsFlyer and Singular ingest postback events via GET routes. The AppsFlyer webhook validates client IPs against `APPSFLYER_IP_RANGES`, parses the `appId` and `partnerEventId`, and matches the installation via Prisma. Singular maps incoming event names through `singularToDubEvent` before dispatching leads or sales.

```typescript
const singularToDubEvent = {
  activated: "lead",
  sng_complete_registration: "lead",
  sng_subscribe: "sale",
  sng_ecommerce_purchase: "sale",
  __iap__: "sale", // In-app purchase
  "Copy GAID": "lead", // Singular Device Assist
  "copy IDFA": "lead", // Singular Device Assist
};
```

Sources: [apps/web/app/ee/api/appsflyer/webhook/route.ts:37-79](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L37-L79), [apps/web/app/ee/api/singular/webhook/route.ts:15-23](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L15-L23), [apps/web/app/ee/api/singular/webhook/route.ts:40-107](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L40-L107)

### Shopify Pixel and Order Caching

The Shopify pixel endpoint receives client-side pixel events (`clickId` and `checkoutToken`), enforces rate limiting via Upstash, validates the click event, and caches the association in `shopifyCheckoutCache` before triggering the order processing job.

```mermaid
sequenceDiagram
  autonumber
  participant Client as Shopify Pixel
  participant API as POST /api/shopify/pixel
  participant Cache as shopifyCheckoutCache
  participant Job as tryDispatchShopifyOrderJob

  Client->>API: POST { clickId, checkoutToken, shopDomain }
  API->>API: Parse body & check rate limit
  API->>API: Verify click event via getClickEvent()
  API->>Cache: shopifyCheckoutCache.set({ checkoutToken, fields: { clickId } })
  Cache-->>API: Stored checkout
  API->>Job: tryDispatchShopifyOrderJob({ checkoutToken, checkout })
  API-->>Client: 200 OK (CORS headers)
```

Sources: [apps/web/app/ee/api/shopify/pixel/route.ts:14-77](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L14-L77)

> [!WARNING]
> If either `checkoutToken` or `clickId` is missing from the incoming Shopify pixel payload, the request is immediately acknowledged with an OK response and skipped without performing attribution.

Sources: [apps/web/app/ee/api/shopify/pixel/route.ts:37-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L37-L45)

### Stripe Customer Sync and Promotion Code Attribution

Stripe webhooks synchronize customer records and handle checkout completions. When processing customer creation or updates via `syncCustomer`, the engine checks metadata for `dubClickId` and an external ID. If a customer checks out using a promotion code without an existing attribution link, `attributeViaPromotionCodeId` resolves the Stripe promotion code ID to a discount code in Dub, records a fake click event, and provisions the customer and lead event.

```typescript
export async function attributeViaPromotionCodeId({
  promotionCodeId,
  workspace,
  mode,
  customerDetails,
}: {
  promotionCodeId: string;
  workspace: Pick<
    Project,
    "id" | "defaultProgramId" | "stripeConnectId" | "webhookEnabled"
  >;
  mode: StripeMode;
  customerDetails: PromoCodeCustomerDetails;
}) {
  const promotionCode = await getPromotionCode({
    promotionCodeId,
    stripeAccountId: workspace.stripeConnectId!,
    mode,
  });
  // ... resolves discountCode, records fake click, and creates customer
}
```

Sources: [apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:22-86](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/utils/sync-customer.ts#L22-L86), [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:30-70](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L30-L70)

| Integration Handler | Inbound Route / Method | Auth / Verification Mechanism | Target Entity / Action |
| :--- | :--- | :--- | :--- |
| **AppsFlyer Webhook** | `GET /api/appsflyer/webhook` | `APPSFLYER_IP_RANGES` validation | `trackLead()` or `trackSale()` |
| **Singular Webhook** | `GET /api/singular/webhook` | `SINGULAR_IP_RANGES` validation | `trackSingularLeadEvent()` or `trackSingularSaleEvent()` |
| **Shopify Pixel** | `POST /api/shopify/pixel` | Upstash Rate Limit & CORS headers | `shopifyCheckoutCache` & order job dispatcher |
| **Stripe Customer Sync** | Customer Webhooks (`syncCustomer`) | Stripe Signature & Metadata extraction | Prisma `Customer` upsert & lead creation |
| **Stripe Promotion Code** | Checkout Session (`attributeViaPromotionCodeId`) | Stripe Promotion ID lookup | `recordFakeClick()` & discount code mapping |

Sources: [apps/web/app/ee/api/appsflyer/webhook/route.ts:37-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L37-L49), [apps/web/app/ee/api/singular/webhook/route.ts:40-52](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L40-L52), [apps/web/app/ee/api/shopify/pixel/route.ts:21-54](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L21-L54), [apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:31-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/utils/sync-customer.ts#L31-L62), [apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:30-80](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts#L30-L80)

## Google Ads Conversion Uploads

### Overview

The Google Ads integration handles server-side conversion uploads and click conversion synchronization by communicating with the Google Ads API and Data Manager service. When a conversion payload is processed, the system extracts advertising identifiers such as `gclid`, `gbraid`, or `wbraid` from click URLs, currency units are normalized, and jobs are queued or executed against Google Ads endpoints.

Sources: [apps/web/lib/integrations/google-ads/api.ts:9-25](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L9-L25), [apps/web/lib/integrations/google-ads/upload-conversion.ts:21-47](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/upload-conversion.ts#L21-L47)

### Queueing and Execution Call-Chain

Conversion uploads follow a structured execution path from inbound requests down to API data ingestion. The operation proceeds through the following call chain:

`queueGoogleAdsConversionUpload()` → `qstash.publishJSON()` → `POST /api/google-ads/upload-conversion` → `uploadGoogleAdsConversion()` → `GoogleAdsApi.uploadClickConversion()` → `dataManagerFetch()`

1. `queueGoogleAdsConversionUpload()` validates that the click URL contains a valid click identifier (`gclid`, `gbraid`, or `wbraid`) and that the workspace has the Google Ads integration installed. It adjusts non-zero-decimal currency values and publishes a payload via QStash with a deduplication ID.
2. The endpoint handler `POST /api/google-ads/upload-conversion` receives the cron-triggered payload and delegates execution to `uploadGoogleAdsConversion()`.
3. `uploadGoogleAdsConversion()` retrieves installed integration settings from Prisma, matches the event name against lead or sale conversion mappings, and initializes a `GoogleAdsApi` client instance using OAuth tokens.
4. `GoogleAdsApi.uploadClickConversion()` constructs operating account destinations, ad identifiers, and event metadata (formatting timestamps via `formatGoogleAdsEventTimestamp`), and executes the upload through `dataManagerFetch()` targeting the `events:ingest` path.

Sources: [apps/web/lib/integrations/google-ads/upload-conversion.ts:49-210](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/upload-conversion.ts#L49-L210), [apps/web/app/ee/api/google-ads/upload-conversion/route.ts:8-13](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/google-ads/upload-conversion/route.ts#L8-L13), [apps/web/lib/integrations/google-ads/api.ts:443-503](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L443-L503)

> [!WARNING]
> New integrations must utilize the Data Manager API (`events:ingest`) rather than the legacy `ConversionUploadService.UploadClickConversions` method when uploading offline click conversions.

Sources: [apps/web/lib/integrations/google-ads/api.ts:441-442](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L441-L442)

### Account Resolution and Conversion Actions

When listing conversion actions or authenticating requests across manager hierarchies, the system evaluates candidate login customer IDs using `getLoginCustomerIdCandidates()` to avoid permission errors.

```typescript
const candidates = getLoginCustomerIdCandidates({
  customers: currentSettings.customers,
  selectedCustomerId: customerId,
  loginCustomerId: currentSettings.loginCustomerId,
});
```

Sources: [apps/web/app/ee/api/google-ads/conversion-actions/route.ts:57-61](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/google-ads/conversion-actions/route.ts#L57-L61), [apps/web/lib/integrations/google-ads/api.ts:541-595](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L541-L595)

| Google Ads API Method | Endpoint / Query | Purpose | Return Type |
| :--- | :--- | :--- | :--- |
| `listUploadClickConversionActions` | `searchStream` (`SELECT conversion_action.id, ... WHERE conversion_action.type = UPLOAD_CLICKS AND conversion_action.status = ENABLED`) | Queries enabled upload click conversion actions for a customer | `Promise<GoogleAdsConversionAction[]>` |
| `uploadClickConversion` | `dataManagerFetch` (`events:ingest`) | Uploads an offline click conversion with retry handling | `Promise<{ requestId: string }>` |

Sources: [apps/web/lib/integrations/google-ads/api.ts:415-440](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L415-L440), [apps/web/lib/integrations/google-ads/api.ts:443-503](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/api.ts#L443-L503)

> [!TIP]
> During upload execution, the system retries failed requests up to three times with exponential backoff (`1000 * Math.pow(2, attempt)`) before marking the conversion upload as failed.

Sources: [apps/web/lib/integrations/google-ads/upload-conversion.ts:196-223](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/upload-conversion.ts#L196-L223)

## Attribution Replay and Historical Analytics

### Customer Reattribution and Historical Event Feeds

Customer reattribution workflows allow moving customer entities and reconciling their event histories between distinct identifiers, while historical analytics mechanisms retrieve and parse customer conversion events from Tinybird and MySQL.

The customer reattribution lifecycle manages stub verification, event planning, and transactional record recreation. `isReattributedCustomerStub()` validates whether a customer record is a stub by checking prefix patterns against `externalId` (`reattributed_`, `dummy_`, `retired_`) alongside null checks for `partnerId`, `linkId`, and `programId`. `getCustomerReattributeEvents()` fetches events for a given customer ID via `getCustomerEventsTB()` up to `CUSTOMER_REATTRIBUTION_EVENTS_LIMIT` (500), filtering for entries where the `event` property is strictly `"click"`, `"lead"`, or `"sale"`. `loadReattributeEventPlan()` evaluates old and new customer events to build an event plan capturing click existence, lead counts, sale counts, total sale amounts, and timestamps.

```typescript
export async function loadReattributeEventPlan({
  oldCustomerId,
  newCustomerId,
}: {
  oldCustomerId: string;
  newCustomerId: string;
}): Promise<ReattributeEventPlan> {
  const [oldEvents, newEvents] = await Promise.all([
    getCustomerReattributeEvents(oldCustomerId),
    getCustomerReattributeEvents(newCustomerId),
  ]);

  if (oldEvents.length >= CUSTOMER_REATTRIBUTION_EVENTS_LIMIT) {
    throw new Error(
      `Customer ${oldCustomerId} has too many events to reattribute (limit ${CUSTOMER_REATTRIBUTION_EVENTS_LIMIT}).`,
    );
  }

  const sourceEvents = oldEvents.length > 0 ? oldEvents : newEvents;
  const clickEvent = sourceEvents.find((event) => event.event === "click");
  const leadEvent = sourceEvents.find((event) => event.event === "lead");
  const saleEvents = sourceEvents.filter((event) => event.event === "sale");

  return {
    hasClick: Boolean(clickEvent),
    hasLead: Boolean(leadEvent),
    leadCount: leadEvent ? 1 : 0,
    saleCount: saleEvents.length,
    saleAmount: saleEvents.reduce(
      (sum, event) => sum + (event.saleAmount ?? 0),
      0,
    ),
    leadTimestamp: leadEvent?.timestamp ?? null,
    saleTimestamp: saleEvents[saleEvents.length - 1]?.timestamp ?? null,
  };
}
```

Sources: [apps/web/lib/api/customers/reattribute-customer.ts:77-238](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts#L77-L238)

> [!WARNING]
> If a customer exceeds `CUSTOMER_REATTRIBUTION_EVENTS_LIMIT` (500 events) during reattribution planning, an error is thrown to prevent unbounded payload processing and memory exhaustion.

Sources: [apps/web/lib/api/customers/reattribute-customer.ts:22-220](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts#L22-L220)

### Batch Backfills and Historical Ingestion

The Framer batch backfill cron endpoint (`POST /api/cron/framer/backfill-leads-batch`) validates workspace authorization against `FRAMER_WORKSPACE_ID` (`clsvopiw0000ejy0grp821me0`), parses inbound request payloads via Zod, and queries Tinybird pipes alongside Prisma databases to process historical lead and sale events.

```typescript
export const POST = withWorkspace(
  async ({ req, workspace }) => {
    try {
      if (workspace.id !== FRAMER_WORKSPACE_ID) {
        throw new DubApiError({
          code: "unauthorized",
          message: "Unauthorized",
        });
      }

      const originalPayload = schema.parse(
        await parseRequestBody(req),
      ) as PayloadItem[];
```

Sources: [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts:56-69](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts#L56-L69)

> [!TIP]
> Background updates to link statistics during batch backfills are delegated via Vercel's `waitUntil()` helper function to ensure immediate HTTP response return without blocking client connections.

Sources: [apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts:331-379](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/framer/backfill-leads-batch/route.ts#L331-L379)

### Customer Conversion Event Retrieval

`getCustomerEvents()` aggregates customer telemetry by querying Tinybird event data and hydrating link records from MySQL via `getLinksMap()`. It normalizes timestamps to UTC, maps processed regional and referer fields, parses click schemas, decodes case-sensitive links, and parses lead or sale metadata depending on the specific event type.

| Function Name | Source File | Purpose | Return Type |
| :--- | :--- | :--- | :--- |
| `getCustomerEvents` | `apps/web/lib/analytics/get-customer-events.ts` | Retrieves and normalizes customer click, lead, and sale events with link metadata | `Promise<ParsedEvent[]>` |
| `getCustomerReattributeEvents` | `apps/web/lib/api/customers/reattribute-customer.ts` | Fetches and filters customer reattribution events up to the event limit | `Promise<CustomerTBEvent[]>` |
| `loadReattributeEventPlan` | `apps/web/lib/api/customers/reattribute-customer.ts` | Compiles event totals, counts, and timestamps for customer reattribution | `Promise<ReattributeEventPlan>` |
| `recreateCustomerForReattribution` | `apps/web/lib/api/customers/reattribute-customer.ts` | Atomically updates old customer stubs and creates new reattributed customer records | `Promise<Customer>` |

Sources: [apps/web/lib/analytics/get-customer-events.ts:13-84](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/get-customer-events.ts#L13-L84), [apps/web/lib/api/customers/reattribute-customer.ts:98-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts#L98-L171), [apps/web/lib/api/customers/reattribute-customer.ts:204-238](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts#L204-L238)

## Related

- [Tinybird Analytics Engine](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/tinybird-analytics-engine)
- [Commission Rules and Rewards](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/commission-rules-and-rewards)


## Sitemap

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