---
title: "Embeddable Referrals"
description: "Embeddable Referrals provide a secure mechanism for integrating white-labeled partner and affiliate program dashboards directly into external web applications via iframe wrappers and React componen..."
last_updated: "2026-10-12T07:00:10.116617+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/administration-and-api/embeddable-referrals"
---

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

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

- [apps/web/app/ee/app.dub.co/embed/referrals/token.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/token.tsx)
- [apps/web/app/ee/api/embed/referrals/token/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/token/route.ts)
- [apps/web/app/api/tokens/embed/referrals/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/tokens/embed/referrals/route.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/page-client.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsx)
- [apps/web/app/api/user/referrals-token/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/user/referrals-token/route.ts)
- [apps/web/lib/embed/referrals/auth.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/auth.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/page.tsx)
- [apps/web/app/ee/app.dub.co/embed/referrals/quickstart.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/quickstart.tsx)
- [apps/web/lib/openapi/embed-tokens/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/embed-tokens/index.ts)
- [apps/web/app/ee/api/embed/referrals/links/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/links/route.ts)
- [apps/web/app/app.dub.co/dashboard/account/settings/referrals/page-client.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/account/settings/referrals/page-client.tsx)
- [apps/web/app/ee/api/embed/referrals/events/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/events/route.ts)
- [apps/web/app/ee/api/embed/referrals/earnings/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/earnings/route.ts)
- [apps/web/lib/embed/referrals/token-class.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/token-class.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts)
- [apps/web/app/ee/api/embed/referrals/earnings/timeseries/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/earnings/timeseries/route.ts)
- [apps/web/app/ee/api/embed/referrals/bounties/bountyId/submissions/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/bounties/%5BbountyId%5D/submissions/route.ts)
- [apps/web/app/ee/api/embed/referrals/analytics/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/analytics/route.ts)
- [apps/web/lib/openapi/embed-tokens/create-referrals-embed-token.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/embed-tokens/create-referrals-embed-token.ts)
- [apps/web/app/ee/api/embed/referrals/bounties/bountyId/upload/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/bounties/%5BbountyId%5D/upload/route.ts)
- [packages/embeds/react/src/embed.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/embeds/react/src/embed.tsx)
- [apps/web/lib/middleware/embed.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/embed.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx)
- [apps/web/app/ee/partners.dub.co/dashboard/programs/programSlug/enrolled/referrals/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/%5BprogramSlug%5D/(enrolled)/referrals/page.tsx)
- [apps/web/app/ee/app.dub.co/embed/referrals/links.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/links.tsx)
- [apps/web/app/ee/app.dub.co/embed/use-embed-token.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/use-embed-token.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/activity.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/activity.tsx)
- [packages/embeds/react/src/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/embeds/react/src/index.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/earnings.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx)
- [apps/web/app/app.dub.co/dashboard/account/settings/referrals/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/account/settings/referrals/page.tsx)
</details>

## Overview

Embeddable Referrals provide a secure mechanism for integrating white-labeled partner and affiliate program dashboards directly into external web applications via iframe wrappers and React components. By minting cryptographically bound tokens backed by Redis storage, platforms can authorize external tenants to track link performance, view earnings, manage referral links, and submit bounties without exposing internal credentials.

> [!NOTE]
> Embedding requires a valid public embed token passed through the authorization header or query parameters to authenticate embedded partner contexts against backend API routes.

Sources: [apps/web/lib/embed/referrals/token-class.ts:13-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/token-class.ts#L13-L33), [packages/embeds/react/src/embed.tsx:8-12](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/embeds/react/src/embed.tsx#L8-L12), [apps/web/lib/embed/referrals/auth.ts:33-128](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/auth.ts#L33-L128)

## Embed Token Minting

### Token Generation and Storage

The token minting process is coordinated through the `ReferralsEmbedToken` class, which uses Upstash Redis storage to persist session properties mapped to a unique public identifier. When `create()` is invoked with a `programId` and `partnerId`, a secure identifier is generated using `createId()` prefixed with `EMBED_PUBLIC_TOKEN_PREFIX`. This token is stored in Redis containing the JSON-serialized payload along with an expiration configured by `EMBED_PUBLIC_TOKEN_EXPIRY`, enforcing non-collision requirements via the `nx` option.

```typescript
class ReferralsEmbedToken {
  async create(props: ReferralsEmbedTokenProps) {
    const publicToken = createId({
      prefix: EMBED_PUBLIC_TOKEN_PREFIX,
    });

    await redis.set(publicToken, JSON.stringify(props), {
      ex: EMBED_PUBLIC_TOKEN_EXPIRY,
      nx: true,
    });

    return {
      publicToken,
      expires: new Date(Date.now() + EMBED_PUBLIC_TOKEN_EXPIRY * 1000),
    };
  }

  async get(token: string) {
    return await redis.get<ReferralsEmbedTokenProps>(token);
  }
}
```
Sources: [apps/web/lib/embed/referrals/token-class.ts:13-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/token-class.ts#L13-L33)

> [!WARNING]
> Stored embed tokens rely entirely on `EMBED_PUBLIC_TOKEN_EXPIRY` for time-to-live management in Redis. If the Redis key expires, subsequent token resolution calls will fail to retrieve the underlying `programId` and `partnerId` mapping.

### Server Endpoints and Upsert Workflows

Two primary API routes handle token generation depending on the execution context. The workspace-level endpoint at `/tokens/embed/referrals` validates requests using `withWorkspace` requiring `links.write` permissions and an `advanced` or `enterprise` plan. It attempts to locate an existing `ProgramEnrollment` by `partnerId` or `tenantId`. If no enrollment exists but partner properties are provided, it resolves or creates the partner record and enrolls them into the default program before minting the token.

```typescript
// POST /api/tokens/embed/referrals - create a new embed token for the given partner/tenant
export const POST = withWorkspace(
  async ({ workspace, req, session }) => {
    const programId = getDefaultProgramIdOrThrow(workspace);

    const {
      partnerId,
      tenantId,
      partner: partnerProps,
    } = createReferralsEmbedTokenSchema.parse(await parseRequestBody(req));

    const finalTenantId =
      tenantId || partnerProps?.tenantId || partnerProps?.linkProps?.tenantId;

    let programEnrollment: Pick<ProgramEnrollment, "partnerId"> | null = null;

    // find the program enrollment for the given partnerId or tenantId
    if (partnerId || finalTenantId) {
      programEnrollment = await prisma.programEnrollment.findUnique({
        where: partnerId
          ? { partnerId_programId: { partnerId, programId } }
          : { tenantId_programId: { tenantId: finalTenantId!, programId } },
        select: {
          partnerId: true,
        },
      });
    }

    // if there is no programEnrollment associated with the partnerId or tenantId, and partnerProps are provided
    // check if the partner exists based on the email – if not, create them, if yes, enroll them
    if (!programEnrollment && partnerProps) {
      const program = await prisma.program.findUniqueOrThrow({
        where: {
          id: programId,
        },
        select: {
          id: true,
          workspaceId: true,
          defaultFolderId: true,
          domain: true,
          url: true,
          defaultGroupId: true,
        },
      });

      const partner = await prisma.partner.findUnique({
        where: {
          email: partnerProps.email,
        },
        include: {
          programs: {
            where: {
              programId,
            },
          },
        },
      });

      // partner does not exist, we need to create them OR
      // partner exists but is not enrolled in the program, we need to enroll them
      if (!partner || partner.programs.length === 0) {
        const { linkProps: link, ...partner } = partnerProps;

        const enrolledPartner = await createAndEnrollPartner({
          workspace,
          program,
          partner: {
            ...partner,
            ...(finalTenantId && { tenantId: finalTenantId }),
          },
          link: {
            ...link,
            ...(finalTenantId && { tenantId: finalTenantId }),
          },
          userId: session.user.id,
        });

        programEnrollment = {
          partnerId: enrolledPartner.id,
        };
```
Sources: [apps/web/app/api/tokens/embed/referrals/route.ts:15-122](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/tokens/embed/referrals/route.ts#L15-L122)

The user-level route at `/api/user/referrals-token` evaluates session state, checking paid workspace allocations, reputation criteria such as earned commissions and ban status via `partnerHasEarnedCommissions` and `partnerIsNotBanned`, and account age thresholds before issuing a public token for the user tenant.

```typescript
export const GET = withSession(async ({ session }) => {
  const user = await prisma.user.findUniqueOrThrow({
    where: {
      id: session.user.id,
    },
    include: {
      partners: {
        include: {
          partner: {
            include: {
              programs: true,
            },
          },
        },
      },
      projects: {
        where: {
          project: {
            plan: {
              not: "free",
            },
          },
        },
      },
    },
  });
  const paidWorkspaces = user.projects.map((project) => project);

  // for free users, need to do some extra checks
  if (paidWorkspaces.length === 0) {
    // for partners with free workspaces, they are only eligible if they have a good reputation
    const programEnrollments = user.partners[0]
      ? user.partners[0].partner.programs
      : [];
    if (
      programEnrollments.length > 0 &&
      !(
        partnerHasEarnedCommissions(
          programEnrollments as ProgramEnrollmentsForDiscoverability,
        ) && partnerIsNotBanned(programEnrollments)
      )
    ) {
      return NextResponse.json({ publicToken: null });
    }

    // for regular free users, don't allow them to join the referral program if they've just joined (within the last 30 days)
    if (user.createdAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)) {
      return NextResponse.json({ publicToken: null });
    }
  }

  const { publicToken } = await dub.embedTokens.referrals({
    tenantId: session.user.id,
    partner: {
      name: session.user.name || session.user.email,
      email: session.user.email,
      image: session.user.image || null,
      tenantId: session.user.id,
      groupId: "grp_1K2QJWRQ917XX2YR5VHQ1RRC5", // User Referrals group
    },
  });

  return NextResponse.json({ publicToken });
});
```
Sources: [apps/web/app/api/user/referrals-token/route.ts:11-74](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/user/referrals-token/route.ts#L11-L74)

### OpenAPI Specification and Operations

The public API specification for embed token creation is defined using `zod-openapi`. The route registers under `/tokens/embed/referrals` with the `createReferralsEmbedToken` operation object, specifying request body validation schemas and response structures.

| Operation Property | Value / Schema | Description |
| :--- | :--- | :--- |
| `operationId` | `createReferralsEmbedToken` | Unique identifier for SDK generation |
| `x-speakeasy-name-override` | `referrals` | Speakeasy client naming override |
| `summary` | "Create a referrals embed token" | Short description of endpoint purpose |
| `requestBody` | `createReferralsEmbedTokenSchema` | JSON schema validating partner and tenant payload |
| `responses` | `201`, `...openApiErrorResponses` | Returns `ReferralsEmbedTokenSchema` on success |
| `tags` | `["Embed Tokens"]` | OpenAPI categorization tag |
| `security` | `[{ token: [] }]` | API key security requirement |

```typescript
export const createReferralsEmbedToken: ZodOpenApiOperationObject = {
  operationId: "createReferralsEmbedToken",
  "x-speakeasy-name-override": "referrals",
  summary: "Create a referrals embed token",
  description:
    "Create a referrals embed token for the given partner/tenant. The endpoint first attempts to locate an existing enrollment using the provided tenantId. If no enrollment is found, it resolves the partner by email and creates a new enrollment as needed. This results in an upsert-style flow that guarantees a valid enrollment and returns a usable embed token.",
  requestBody: {
    content: {
      "application/json": {
        schema: createReferralsEmbedTokenSchema,
      },
    },
  },
  responses: {
    "201": {
      description: "The created public embed token.",
      content: {
        "application/json": {
          schema: ReferralsEmbedTokenSchema,
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Embed Tokens"],
  security: [{ token: [] }],
};
```
Sources: [apps/web/lib/openapi/embed-tokens/create-referrals-embed-token.ts:8-34](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/embed-tokens/create-referrals-embed-token.ts#L8-L34), [apps/web/lib/openapi/embed-tokens/index.ts:1-7](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/embed-tokens/index.ts#L1-L7)

## Token Verification Middleware

### Authentication Wrappers and Route Handling

```typescript
export const withReferralsEmbedToken = (
  handler: WithReferralsEmbedTokenHandler,
) => {
  return withAxiom(
    async (
      req,
      { params: initialParams }: { params: Promise<Record<string, string>> },
    ) => {
      const params = (await initialParams) || {};
      const requestHeaders = await headers();
      let responseHeaders = new Headers();

      try {
        const rateLimit = 60;
        const searchParams = getSearchParams(req.url);
        const embedToken = requestHeaders.get("Authorization")?.split(" ")[1];

        if (!embedToken) {
          throw new DubApiError({
            code: "unauthorized",
            message: "Embed public token not found in the request.",
          });
        }

        const { programId, partnerId } =
          (await referralsEmbedToken.get(embedToken)) ?? {};

        if (!programId || !partnerId) {
          throw new DubApiError({
            code: "unauthorized",
            message: "Invalid embed public token.",
          });
        }
```
Sources: [apps/web/lib/embed/referrals/auth.ts:33-65](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/auth.ts#L33-L65)

The `withReferralsEmbedToken` function acts as a higher-order wrapper securing referral embed API routes. It intercepts incoming requests to extract the public token from the `Authorization` request header using a Bearer scheme split. When the token is missing, it throws a `DubApiError` with the unauthorized code and a specific message. If the token is successfully extracted, it queries the underlying storage via `referralsEmbedToken.get(embedToken)` to retrieve the associated `programId` and `partnerId`.

> [!WARNING]
> If a request provides an invalid or expired embed token where `programId` or `partnerId` cannot be resolved from storage, the wrapper immediately halts execution and returns an unauthorized response.

```typescript
const { success, limit, reset, remaining } = await ratelimit(
  rateLimit,
  "1 m",
).limit(embedToken);

responseHeaders.set("Retry-After", reset.toString());
responseHeaders.set("X-RateLimit-Limit", limit.toString());
responseHeaders.set("X-RateLimit-Remaining", remaining.toString());
responseHeaders.set("X-RateLimit-Reset", reset.toString());

if (!success) {
  throw new DubApiError({
    code: "rate_limit_exceeded",
    message: "Too many requests.",
  });
}

```
Sources: [apps/web/lib/embed/referrals/auth.ts:67-83](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/auth.ts#L67-L83)

Rate limiting is enforced on every verified token using Upstash Redis with a fixed window of 60 requests per minute. Standard rate-limit metadata headers are injected into the response collection, including `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. If the limit is exceeded, a `rate_limit_exceeded` error is thrown.

### Database Resolution and Context Injection

```typescript
const { program, links, partnerGroup, ...programEnrollment } =
  await prisma.programEnrollment.findUniqueOrThrow({
    where: {
      partnerId_programId: {
        partnerId,
        programId,
      },
    },
    include: {
      links: {
        orderBy: [
          {
            saleAmount: "desc",
          },
          {
            leads: "desc",
          },
          {
            clicks: "desc",
          },
        ],
      },
      program: true,
      partnerGroup: true,
    },
  });

if (!partnerGroup) {
  throw new DubApiError({
    code: "forbidden",
    message:
      "You’re not part of any group yet. Please reach out to the program owner to be added.",
  });
}

return await handler({
  req,
  params,
  searchParams,
  program,
  programEnrollment,
  group: partnerGroup as PartnerGroupProps,
  links,
  embedToken,
});
```
Sources: [apps/web/lib/embed/referrals/auth.ts:84-128](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/auth.ts#L84-L128)

Once rate limits and token validity are confirmed, the middleware queries Prisma to locate the corresponding `ProgramEnrollment` record using a composite unique identifier combining `partnerId` and `programId`. The query includes relations for `links` (ordered descending by sale amount, leads, then clicks), `program`, and `partnerGroup`. 

| Relation Included | Order Criteria / Fields | Purpose |
| :--- | :--- | :--- |
| `links` | `saleAmount: desc`, `leads: desc`, `clicks: desc` | Retrieves partner referral links sorted by performance |
| `program` | Full relation | Attaches parent referral program configuration |
| `partnerGroup` | Full relation | Validates group assignment and partner tier settings |

> [!CAUTION]
> If the `partnerGroup` relation resolves to null on the enrollment record, the wrapper throws a `forbidden` error stating that the user is not part of any group yet and must reach out to the program owner.

```typescript
// GET /api/embed/referrals/token - get the referrals embed token
export const GET = withReferralsEmbedToken(async ({ embedToken }) => {
  return NextResponse.json(embedToken);
});
```
Sources: [apps/web/app/ee/api/embed/referrals/token/route.ts:4-7](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/token/route.ts#L4-L7)

Target handlers such as the GET route at `/api/embed/referrals/token` receive the fully populated context object containing `req`, `params`, `searchParams`, `program`, `programEnrollment`, `group`, `links`, and `embedToken`.

### Edge Request Path Rewriting

```typescript
import { NextRequest, NextResponse } from "next/server";
import { parse } from "./utils/parse";

export function EmbedMiddleware(req: NextRequest) {
  const { path, searchParamsObj, fullPath } = parse(req);

  if (path.startsWith("/embed/support-chat")) {
    return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
  }

  if (searchParamsObj.token) {
    return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
  }

  // TODO: Show token expiry page
  return NextResponse.redirect(new URL("/", req.url));
}
```
Sources: [apps/web/lib/middleware/embed.ts:1-17](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/embed.ts#L1-L17)

At the edge network layer, `EmbedMiddleware` inspects incoming `NextRequest` paths and parameters. If the path starts with `/embed/support-chat` or if a query parameter `token` is present on the request, it rewrites the destination URL to point to `app.dub.co` combined with the full requested path. Otherwise, requests fall through to a default redirect to the root URL.

## Client Embedding Library

### React Integration and Component Architecture

```tsx
"use client";

import { DubEmbedOptions, init } from "@dub/embed-core";
import { HTMLProps, memo, useEffect, useId, useRef } from "react";

type Options = Omit<DubEmbedOptions, "token">;

type DubEmbedProps = {
  token: DubEmbedOptions["token"];
  data: DubEmbedOptions["data"];
  options?: Options;
} & HTMLProps<HTMLDivElement>;

export const DubEmbed = memo(
  ({ token, data, options, ...rest }: DubEmbedProps) => (
    <DubEmbedInner options={{ ...options, token, data }} {...rest} />
  ),
);
```
Sources: [packages/embeds/react/src/embed.tsx:1-18](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/embeds/react/src/embed.tsx#L1-L18)

The `@dub/embed-react` package exposes the `DubEmbed` component, which wraps client-side embedding initialization for React parent applications. The component accepts `token`, `data`, and optional configuration properties alongside standard `HTMLDivElement` HTML attributes. It utilizes `React.memo` to prevent unnecessary re-renders when parent states change without modifying embed parameters.

| Prop Name | Type | Purpose |
| :--- | :--- | :--- |
| `token` | `DubEmbedOptions["token"]` | Public authentication token minted for the embed session |
| `data` | `DubEmbedOptions["data"]` | Dataset identifier (e.g., `"referrals"`) loaded by the widget |
| `options` | `Options` | Optional overrides excluding the token property |

> [!NOTE]
> `DubEmbed` components must be rendered inside client-side components designated with the `"use client"` directive, as they rely on React hooks and DOM ref attachments.

### Lifecycle and DOM Initialization

```tsx
function DubEmbedInner({
  options,
  ...rest
}: { options: DubEmbedOptions } & HTMLProps<HTMLDivElement>) {
  const id = useId();
  const rootRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!rootRef.current) return;

    const { destroy } =
      init({
        root: rootRef.current,
        ...options,
      }) || {};

    return () => destroy?.();
  }, [id, JSON.stringify(options)]);

  return <div {...rest} ref={rootRef} />;
}
```
Sources: [packages/embeds/react/src/embed.tsx:20-40](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/embeds/react/src/embed.tsx#L20-L40)

The inner component `DubEmbedInner` manages the lifecycle of the underlying core embedding script via a local `useRef` pointing to a container `div`. It invokes the `init` method imported from `@dub/embed-core`, passing the root DOM element and merged options.

```mermaid
sequenceDiagram
    participant Parent as Parent App
    participant Embed as DubEmbedInner
    participant Core as @dub/embed-core

    Parent->>Embed: Mount component with token & data
    Embed->>Embed: Generate unique id & attach useRef
    Embed->>Core: init({ root: rootRef.current, ...options })
    Core-->>Embed: Return destroy function
    Note over Embed,Core: Component unmounts or options change
    Embed->>Core: destroy() cleanup
```
Sources: [packages/embeds/react/src/embed.tsx:20-40](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/embeds/react/src/embed.tsx#L20-L40)

> [!WARNING]
> The `useEffect` hook depends on `JSON.stringify(options)` and `id`. Modifying configuration objects inline without memoization in the parent application can trigger repeated destruction and re-initialization cycles of the core embed instance.

### Parent Application Consumption

```tsx
"use client";

import LayoutLoader from "@/ui/layout/layout-loader";
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
import { DubEmbed } from "@dub/embed-react";
import { CursorRays, Hyperlink, InvoiceDollar, UserCheck } from "@dub/ui/icons";
import { fetcher } from "@dub/utils";
import { useSession } from "next-auth/react";
import useSWRImmutable from "swr/immutable";

export function ReferralsPageClient() {
  const { data: session, status } = useSession();

  const { data: { publicToken } = {}, isLoading } = useSWRImmutable<{
    publicToken: string;
  }>(session && "/api/user/referrals-token", fetcher, {
    keepPreviousData: true,
  });

  if (status === "loading" || isLoading) {
    return <LayoutLoader />;
  }

  if (!publicToken) {
    return (
      <div className="p-10">
        <AnimatedEmptyState
          title="Refer a friend"
          description="Activate your referral link to share the word about Dub and earn cash rewards"
          className="border-none"
          cardContent={
            <>
              <Hyperlink className="size-4 text-neutral-700" />
              <div className="h-2.5 w-24 min-w-0 rounded-sm bg-neutral-200" />
              <div className="xs:flex hidden grow items-center justify-end gap-1.5 text-neutral-500">
                <CursorRays className="size-3.5" />
                <UserCheck className="size-3.5" />
                <InvoiceDollar className="size-3.5" />
              </div>
            </>
          }
          pillContent="Coming soon"
        />
      </div>
    );
  }

  return <DubEmbed data="referrals" token={publicToken} />;
}
```
Sources: [apps/web/app/app.dub.co/dashboard/account/settings/referrals/page-client.tsx:1-49](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/account/settings/referrals/page-client.tsx#L1-L49)

In dashboard pages such as the referrals settings view, client implementations fetch user session data and retrieve public embed tokens asynchronously using SWR immutable hooks against `/api/user/referrals-token`. While loading, components render layout loaders. If no token is provisioned, they present an empty state UI. Once the token resolves, `DubEmbed` renders with `data="referrals"` and the active `publicToken`.

## Embedded Dashboard Data Resolution

### Server-Side Data Retrieval and Enrollment Resolution

```typescript
export const getReferralsEmbedData = async (token: string) => {
  const { programId, partnerId } = (await referralsEmbedToken.get(token)) ?? {};

  if (!programId || !partnerId) {
    notFound();
  }
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:14-19](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts#L14-L19)

The server action `getReferralsEmbedData` initiates embedded page initialization by extracting the token and retrieving associated metadata from storage via `referralsEmbedToken.get(token)`. If either `programId` or `partnerId` is missing from the stored record, the function immediately triggers a `notFound()` response.

```typescript
const programEnrollment = await getProgramEnrollmentOrThrow({
  partnerId,
  programId,
  include: {
    partner: {
      select: {
        id: true,
        name: true,
        email: true,
        username: true,
        country: true,
        tremendousEmail: true,
        defaultPayoutMethod: true,
        platforms: {
          select: {
            type: true,
            identifier: true,
            verifiedAt: true,
          },
        },
      },
    },
    program: {
      select: {
        id: true,
        name: true,
        slug: true,
        domain: true,
        defaultGroupId: true,
        minPayoutAmount: true,
        termsUrl: true,
        embedData: true,
        resources: true,
      },
    },
    links: {
      include: {
        linkReward: {
          include: {
            clickReward: true,
            leadReward: true,
            saleReward: true,
            discount: true,
          },
        },
      },
    },
    partnerGroup: true,
    clickReward: true,
    leadReward: true,
    saleReward: true,
    referralReward: true,
    customReward: true,
    discount: true,
    programPartnerTags: {
      select: {
        partnerTagId: true,
      },
    },
  },
});

if (!programEnrollment || !programEnrollment.partnerGroup) {
  notFound();
}
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:21-85](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts#L21-L85)

Once the identifiers are resolved, `getProgramEnrollmentOrThrow` queries the database for the active partner program enrollment. The Prisma query requests a selection of partner properties, program definitions, partner groups, reward configurations, and associated links. If the enrollment record or its assigned `partnerGroup` cannot be located, a `notFound()` exception is thrown.

| Relation / Field | Selected Subfields | Purpose |
| :--- | :--- | :--- |
| `partner` | `id`, `name`, `email`, `username`, `country`, `tremendousEmail`, `defaultPayoutMethod`, `platforms` | Identifies affiliate profile and payout preferences |
| `program` | `id`, `name`, `slug`, `domain`, `defaultGroupId`, `minPayoutAmount`, `termsUrl`, `embedData`, `resources` | Configures affiliate program rules and branding resources |
| `links` | `linkReward` (with `clickReward`, `leadReward`, `saleReward`, `discount`) | Resolves custom link-level commission structures |
| `partnerGroup` | All fields (`partnerGroup: true`) | Applies tier-specific group settings and limits |
| `commissions` | `status`, `earnings` (grouped) | Aggregates earnings statistics by commission lifecycle status |

> [!IMPORTANT]
> The database query explicitly includes `clickReward`, `leadReward`, `saleReward`, `referralReward`, `customReward`, and `discount` on the enrollment object to resolve fallback rewards for links lacking individual overrides.

Sources: [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:21-85](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts#L21-L85)

### Aggregation and Parallelized Prisma Queries

```typescript
const { totalClicks, totalLeads, totalConversions } =
  aggregatePartnerLinksStats(links);

const [commissions, bounties] = await Promise.all([
  prisma.commission.groupBy({
    by: ["status"],
    _sum: {
      earnings: true,
    },
    _count: {
      id: true,
    },
    where: {
      earnings: {
        gt: 0,
      },
      programId,
      partnerId,
    },
  }),

  getBountiesForPartner(programEnrollment),
]);
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:100-122](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts#L100-L122)

Following program enrollment retrieval, link statistics are aggregated using `aggregatePartnerLinksStats(links)`. Simultaneously, `Promise.all` executes two parallel database requests: a Prisma `groupBy` query on the `commission` table filtered by `programId` and `partnerId` where `earnings > 0`, and a call to `getBountiesForPartner(programEnrollment)`.

```typescript
earnings: {
  upcoming: commissions.reduce((acc, c) => {
    if (c.status === "pending" || c.status === "processed") {
      return acc + (c._sum.earnings ?? 0);
    }
    return acc;
  }, 0),
  paid: commissions.find((c) => c.status === "paid")?._sum.earnings ?? 0,
  totalCount: commissions.reduce((acc, c) => acc + c._count.id, 0),
},
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:155-164](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts#L155-L164)

The raw commission groups are reduced into summary totals for the dashboard UI. Upcoming earnings accumulate sums where status is either `"pending"` or `"processed"`, paid earnings resolve from records matching the `"paid"` status, and total count aggregates all commission record IDs across groups.

> [!WARNING]
> Commission filtering requires `earnings: { gt: 0 }`. Zero-amount or negative commission entries are omitted from partner earnings totals during aggregation.

Sources: [apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:103-119](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts#L103-L119)

### Page Routing and Suspense Fallback Rendering

```tsx
export default async function ReferralsEmbedPage(props: {
  searchParams: Promise<{
    token: string;
    themeOptions?: string;
    dynamicHeight?: string;
  }>;
}) {
  const searchParams = await props.searchParams;
  const {
    token,
    themeOptions: themeOptionsRaw,
    dynamicHeight: dynamicHeightRaw,
  } = searchParams;

  const themeOptions = parseThemeOptions(themeOptionsRaw);
  const dynamicHeight = !!dynamicHeightRaw && dynamicHeightRaw !== "false";

  return (
    <>
      <Suspense fallback={<EmbedInlineLoading themeOptions={themeOptions} />}>
        <ReferralsEmbedRSC
          token={token}
          themeOptions={themeOptions}
          dynamicHeight={dynamicHeight}
        />
      </Suspense>
      {dynamicHeight && <DynamicHeightMessenger />}
    </>
  );
}
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/page.tsx:9-38](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/page.tsx#L9-L38)

The top-level Next.js Server Component `ReferralsEmbedPage` extracts search parameters (`token`, `themeOptions`, and `dynamicHeight`) asynchronously. It parses theme configurations using `parseThemeOptions` and normalizes the dynamic height boolean flag. It then renders the asynchronous server component `ReferralsEmbedRSC` wrapped inside a `Suspense` boundary with an `EmbedInlineLoading` fallback, alongside a `DynamicHeightMessenger` if enabled.

```tsx
async function ReferralsEmbedRSC({
  token,
  themeOptions,
  dynamicHeight,
}: {
  token: string;
  themeOptions: ThemeOptions;
  dynamicHeight: boolean;
}) {
  const embedData = await getReferralsEmbedData(token);

  return (
    <ReferralsEmbedPageClient
      {...embedData}
      themeOptions={themeOptions}
      dynamicHeight={dynamicHeight}
    />
  );
}
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/page.tsx:40-58](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/page.tsx#L40-L58)

`ReferralsEmbedRSC` awaits `getReferralsEmbedData(token)` to resolve the complete dataset before passing it down to `ReferralsEmbedPageClient`, combining the aggregated payload with client theme options and height constraints.

> [!NOTE]
> The `EmbedInlineLoading` component constructs an inline skeleton matching the dimensions of the referral link widget and reward cards, utilizing the background color specified in the theme options.

Sources: [apps/web/app/ee/app.dub.co/embed/referrals/page.tsx:60-96](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/page.tsx#L60-L96)

## Partner Referral Interactivity

### Link Creation and Validation Endpoint

```typescript
// POST /api/embed/referrals/links – create links for a partner
export const POST = withReferralsEmbedToken(
  async ({ req, programEnrollment, program, links, group }) => {
    if (!ACTIVE_ENROLLMENT_STATUSES.includes(programEnrollment.status)) {
      throw new DubApiError({
        code: "forbidden",
        message:
          "You cannot create links in this program because your enrollment is not active.",
      });
    }

    const { url, key } = createPartnerLinkSchema
      .pick({ url: true, key: true })
      .parse(await parseRequestBody(req));

    if (!program.domain || !program.url) {
      throw new DubApiError({
        code: "bad_request",
        message:
          "This program needs a domain and URL set before creating a link.",
      });
    }

    if (links.length >= group.maxPartnerLinks) {
      throw new DubApiError({
        code: "bad_request",
        message: `You have reached the limit of ${group.maxPartnerLinks} program links.`,
      });
    }

    validatePartnerLinkUrl({ group, url });

```
Sources: [apps/web/app/ee/api/embed/referrals/links/route.ts:25-56](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/links/route.ts#L25-L56)

The embedded partner link route processes link generation requests through the `POST` handler wrapped with `withReferralsEmbedToken`. It first validates that `programEnrollment.status` is included in `ACTIVE_ENROLLMENT_STATUSES`. The handler parses the request body for `url` and `key` using `createPartnerLinkSchema`, confirms that the program has both a `domain` and a `url` configured, and verifies that the existing link count does not exceed `group.maxPartnerLinks`. Finally, it invokes `validatePartnerLinkUrl` to check destination validity against group rules.

```typescript
const [workspaceOwner, partnerGroup, partner] = await Promise.all([
  prisma.projectUsers.findFirst({
    where: {
      projectId: program.workspaceId,
      role: "owner",
    },
    orderBy: {
      createdAt: "desc",
    },
    include: {
      project: {
        select: {
          id: true,
          webhookEnabled: true,
        },
      },
    },
  }),

  prisma.partnerGroup.findUnique({
    where: {
      id: group.id,
    },
    include: {
      partnerGroupDefaultLinks: true,
      utmTemplate: true,
    },
  }),

  prisma.partner.findUnique({
    where: {
      id: programEnrollment.partnerId,
    },
    select: {
      name: true,
    },
  }),
]);

// shouldn't happen but just in case
if (!partnerGroup) {
  throw new DubApiError({
    code: "not_found",
    message: "This partner is not part of a partner group.",
  });
}

const linkUrl = url || partnerGroup.partnerGroupDefaultLinks[0].url;

const { link, error, code } = await processLink({
  payload: {
    key: key || undefined,
    url: linkUrl,
    domain: program.domain,
    programId: program.id,
    folderId: program.defaultFolderId,
    tenantId: programEnrollment.tenantId,
    partnerId: programEnrollment.partnerId,
    trackConversion: true,
  },
  workspace: {
    id: program.workspaceId,
    plan: "business",
    users: [{ role: "owner" }],
  },
  userId: workspaceOwner?.userId,
  skipFolderChecks: true, // can't be changed by the partner
  skipProgramChecks: true, // can't be changed by the partner
  skipExternalIdChecks: true, // can't be changed by the partner
});
```
Sources: [apps/web/app/ee/api/embed/referrals/links/route.ts:57-126](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/links/route.ts#L57-L126)

Once validated, the route queries the workspace owner user, partner group details including default links and UTM templates, and the partner name in parallel. It selects a default destination URL if none is supplied by falling back to `partnerGroup.partnerGroupDefaultLinks[0].url`. It then calls `processLink` with flags set to skip folder, program, and external ID checks, associating [conversion tracking](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-pipeline/conversion-tracking) and tenancy identifiers.

> [!WARNING]
> If a partner's program enrollment status is inactive or if they exceed their tier's `maxPartnerLinks` limit, the API immediately throws a `DubApiError` with code `forbidden` or `bad_request`.

Sources: [apps/web/app/ee/api/embed/referrals/links/route.ts:28-53](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/links/route.ts#L28-L53)

### Leaderboard Presentation and Data SWR

```tsx
export function ReferralsEmbedLeaderboard() {
  const token = useEmbedToken();

  const { data: partners, isLoading } = useSWR<
    z.infer<typeof LeaderboardPartnerSchema>[]
  >(
    "/api/embed/referrals/leaderboard",
    (url) =>
      fetcher(url, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }),
    {
      keepPreviousData: true,
    },
  );
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx:20-36](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx#L20-L36)

The `ReferralsEmbedLeaderboard` component fetches ranked partner performance via SWR from the `/api/embed/referrals/leaderboard` endpoint using an authorization bearer token. It maintains previous data during revalidation and parses rows against `LeaderboardPartnerSchema`.

```tsx
const { table, ...tableProps } = useTable({
  data: partners || [],
  loading: isLoading,
  columns: [
    {
      id: "rank",
      header: "Rank",
      size: 40,
      minSize: 40,
      cell: ({ row }) => {
        return (
          <div className="flex w-8 items-center justify-start gap-1 tabular-nums">
            {row.index + 1}
            {row.index <= 2 && (
              <Crown
                className={cn("size-4", {
                  "text-amber-400": row.index === 0,
                  "text-neutral-400": row.index === 1,
                  "text-yellow-900": row.index === 2,
                })}
              />
            )}
          </div>
        );
      },
    },
    {
      id: "name",
      header: "Partner",
      cell: ({ row }) => {
        return (
          <div className="flex items-center gap-2">
            <PartnerAvatar partner={row.original} className="size-5" />
            <Tooltip content="For privacy reasons, the name of the partner is anonymized.">
              <span className="cursor-help text-sm font-medium decoration-dotted underline-offset-2 hover:underline">
                {generateRandomName(row.original.id)}
              </span>
            </Tooltip>
          </div>
        );
      },
    },
    {
      id: "totalCommissions",
      header: "Earnings",
      cell: ({ row }) => {
        return currencyFormatter(row.original.totalCommissions);
      },
    },
  ],
  emptyState: (
    <AnimatedEmptyState
      title="No partners found"
      description="No partners have been added to this program yet."
      cardContent={() => (
        <>
          <Users className="text-content-default size-4" />
          <div className="bg-bg-emphasis h-2.5 w-24 min-w-0 rounded-sm" />
        </>
      )}
      className="border-none md:min-h-fit"
    />
  ),
  thClassName: "border-l-0",
  tdClassName: "border-l-0",
  resourceName: (plural) => `partner${plural ? "s" : ""}`,
});
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx:38-104](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx#L38-L104)

The table UI configures three explicit columns: rank, partner name, and total commissions earnings. The rank cell displays numeric ranking alongside a `Crown` icon styled in amber, neutral, or yellow-brown for the top three positions. The partner column renders a `PartnerAvatar` alongside an anonymized partner name generated via `generateRandomName(row.original.id)` wrapped in a privacy tooltip. Earnings are formatted using `currencyFormatter`.

> [!NOTE]
> When no partners populate the leaderboard, the table renders an `AnimatedEmptyState` component featuring a users icon and placeholder indicator lines.

Sources: [apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx:88-100](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx#L88-L100)

## Performance and Earnings Telemetry

### Activity Metrics and Timeseries Telemetry

```typescript
const querySchema = partnerProfileAnalyticsQuerySchema.pick({
  event: true,
  start: true,
  end: true,
  groupBy: true,
  saleType: true,
  interval: true,
});

// GET /api/embed/referrals/analytics – get analytics for a partner
export const GET = withReferralsEmbedToken(
  async ({ links, program, searchParams }) => {
    if (links.length === 0) {
      return NextResponse.json([]);
    }

    const parsedQuery = querySchema.parse(searchParams);

    const analytics = await getAnalytics({
      ...parsedQuery,
      workspaceId: program.workspaceId,
      linkId: parseFilterValue(links.map((link) => link.id)),
      dataAvailableFrom: program.startedAt ?? program.createdAt,
    });

    return NextResponse.json(analytics);
  },
```
Sources: [apps/web/app/ee/api/embed/referrals/analytics/route.ts:7-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/analytics/route.ts#L7-L33)

The embedded analytics route handles retrieval of performance metrics via the `GET` method wrapped in `withReferralsEmbedToken`. It parses query parameters using `partnerProfileAnalyticsQuerySchema` restricted to `event`, `start`, `end`, `groupBy`, `saleType`, and `interval`. If the partner has no associated links, the endpoint returns an empty array. Otherwise, it calls `getAnalytics` with the workspace identifier, parsed filter values, and data availability boundaries derived from `program.startedAt` or `program.createdAt`.

```tsx
export function ReferralsEmbedActivity() {
  const {
    group: { brandColor: color },
    stats: { clicks, leads, conversions },
  } = useReferralsEmbedData();

  const token = useEmbedToken();

  const isEmpty = clicks === 0 && leads === 0 && conversions === 0;

  const analyticsSearchParams = new URLSearchParams({
    event: "composite",
    groupBy: "timeseries",
    interval: "1y",
    saleType: "new",
  });

  const { data: analytics } = useSWR<AnalyticsTimeseries[]>(
    !isEmpty &&
      `/api/embed/referrals/analytics?${analyticsSearchParams.toString()}`,
    (url) =>
      fetcher(url, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }),
    {
      keepPreviousData: true,
      dedupingInterval: 60000,
    },
  );
```
Sources: [apps/web/app/ee/app.dub.co/embed/referrals/activity.tsx:10-40](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/activity.tsx#L10-L40)

The `ReferralsEmbedActivity` component renders performance cards for clicks, leads, and conversions. It queries `/api/embed/referrals/analytics` using SWR with a composite event type, yearly interval, and new sale type. When click, lead, and conversion counts are all zero, it renders an `EmptyState` component featuring a `CursorRays` icon and a decorative SVG background gradient.

> [!NOTE]
> Customer email addresses returned by event endpoints are dynamically obfuscated via `obfuscateCustomerEmail` unless `customerDataSharingEnabledAt` is configured on the program enrollment.

Sources: [apps/web/app/ee/api/embed/referrals/events/route.ts:39-68](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/embed/referrals/events/route.ts#L39-L68)

### Commission Earnings and Payout Data

```typescript
const querySchema = getPartnerEarningsQuerySchema
  .pick({
    start: true,
    end: true,
    page: true,
    pageSize: true,
    interval: true,
    timezone: true,
  })
  .extend({
    withTotal: z
      .string()
      .optional()
      .transform((v) => v?.toLowerCase() === "true")
      .default(false),
  });

// GET /api/embed/referrals/earnings – get commissions for a partner from an embed token
export const GET = withReferralsEmbedToken(
  async ({ programEnrollment, searchParams }) => {
    const { withTotal, start, end, pageSize, page, interval, timezone } =
      querySchema.parse(searchParams);

    const [earnings, total] = await Promise.all([
      getEarningsForPartner({
        page,
        
```

## Related

- [Partner Programs](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/partner-programs/partner-programs)
- [REST API](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/administration-and-api/rest-api)


## Sitemap

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