---
title: "OpenAPI and Public REST API"
description: "Dub provides a comprehensive public REST API built on the OpenAPI 3.0 specification, enabling developers to programmatically manage short links, domain names, analytics, and affiliate partner netwo..."
last_updated: "2026-10-05T05:07:35.166923+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/developer-tools/openapi-and-public-rest-api"
---

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

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

- [apps/web/lib/openapi/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/index.ts)
- [apps/web/lib/openapi/track/open.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/track/open.ts)
- [apps/web/app/api/domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts)
- [apps/web/scripts/dev/data.json](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/data.json)
- [apps/web/lib/openapi/links/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/index.ts)
- [apps/web/lib/openapi/partners/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/index.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/app/api/links/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts)
- [apps/web/lib/openapi/analytics/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/analytics/index.ts)
- [apps/web/app/api/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/route.ts)
- [apps/web/lib/openapi/domains/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/domains/index.ts)
- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts)
- [apps/web/app/api/links/metatags/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts)
- [apps/web/app/api/links/iframeable/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts)
- [apps/web/lib/openapi/links/get-links.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-links.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/openapi/bounties/list-bounty-submissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/bounties/list-bounty-submissions.ts)
- [apps/web/lib/openapi/partners/retrieve-analytics.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/retrieve-analytics.ts)
- [packages/utils/src/constants/dub-domains.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/dub-domains.ts)
- [apps/web/lib/openapi/links/get-link-info.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-link-info.ts)
- [packages/cli/src/api/links.ts](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/api/links.ts)
- [apps/web/lib/openapi/links/get-links-count.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-links-count.ts)
- [packages/ui/src/content.ts](https://github.com/blade47/dub/blob/HEAD/packages/ui/src/content.ts)
- [apps/web/lib/zod/schemas/opens.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/opens.ts)
- [apps/web/app/app.dub.co/marketplace/...segments/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/marketplace/%5B%5B...segments%5D%5D/page.tsx)
- [apps/web/lib/openapi/qr/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/qr/index.ts)
- [apps/web/lib/openapi/embed-tokens/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/embed-tokens/index.ts)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dub.ts)
- [apps/web/lib/middleware/api.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts)
- [apps/web/lib/openapi/customers/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/customers/index.ts)
</details>

## Overview

Dub provides a comprehensive public REST API built on the OpenAPI 3.0 specification, enabling developers to programmatically manage short links, domain names, analytics, and affiliate partner networks at scale. The API specification is dynamically generated using `zod-openapi` to unify robust runtime request validation with machine-readable contract documentation. Edge middleware handles request normalization, routing, and CORS policies across all public endpoints, while bearer token authentication secures workspace-level operations.

Sources: [apps/web/lib/openapi/index.ts:1-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/index.ts#L1-L27), [apps/web/lib/openapi/links/get-links.ts:6-46](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-links.ts#L6-L46), [apps/web/lib/middleware/api.ts:1-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts#L1-L16)

## OpenAPI Specification Generation

### Overview

Dub generates its unified OpenAPI 3.0 document programmatically using the `zod-openapi` library, consolidating schema definitions, security schemes, error response components, and path routes into a single export. The generation process starts by passing a configuration object to `createDocument`, defining metadata such as the API title (`Dub API`), version (`0.0.1`), contact details (`support@dub.co`), AGPL-3.0 licensing information, and production servers (`https://api.dub.co`).

Sources: [apps/web/lib/openapi/index.ts:1-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/index.ts#L1-L48)

### Path Composition and Component Registration

The document aggregates path definitions from domain-specific modules, spreading them into the root `paths` object. These include operations for links, analytics, events, tags, folders, domains, tracking, customers, partners, program applications, discount codes, commissions, payouts, embed tokens, QR codes, and bounties. Reusable schemas are registered under `components.schemas`, referencing validation definitions such as `LinkSchema`, `LinkTagSchema`, `FolderSchema`, `DomainSchema`, `DiscountCodeSchema`, `webhookEventSchema`, and `LinkErrorSchema`. 

```typescript
export const document = createDocument({
  openapi: "3.0.3",
  info: {
    title: "Dub API",
    description:
      "Dub is the modern link attribution platform for short links, conversion tracking, and affiliate programs.",
    version: "0.0.1",
    contact: {
      name: "Dub Support",
      email: "support@dub.co",
      url: "https://dub.co/support",
    },
    license: {
      name: "AGPL-3.0 license",
      url: "https://github.com/dubinc/dub/blob/main/LICENSE.md",
    },
  },
  servers: [
    {
      url: "https://api.dub.co",
      description: "Production API",
    },
  ],
  paths: {
    ...linksPaths,
    ...analyticsPath,
    ...eventsPath,
    ...tagsPaths,
    ...foldersPaths,
    ...domainsPaths,
    ...trackPaths,
    ...customersPaths,
    ...partnersPaths,
    ...programApplicationsPaths,
    ...discountCodesPaths,
    ...commissionsPaths,
    ...payoutsPaths,
    ...embedTokensPaths,
    ...qrCodePaths,
    ...bountiesPaths,
  },
  components: {
    schemas: {
      LinkSchema,
      LinkTagSchema,
      FolderSchema,
      DomainSchema,
      DiscountCodeSchema,
      webhookEventSchema,
      LinkErrorSchema,
    },
    securitySchemes: {
      token: {
        type: "http",
        description: "Default authentication mechanism",
        scheme: "bearer",
        "x-speakeasy-example": "DUB_API_KEY",
      },
    },
    responses: {
      ...openApiErrorResponsesComponents,
    },
  },
});
```

Sources: [apps/web/lib/openapi/index.ts:26-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/index.ts#L26-L89)

### API Route Exposure and Caching

The generated OpenAPI specification is exposed to clients via a Next.js App Router API route handler located at `apps/web/app/api/route.ts`. The route handler exports a static configuration flag (`force-static`) and a `GET` function that serializes the `document` object using `NextResponse.json`. To optimize performance and reduce regeneration overhead, caching headers are explicitly attached to the response, setting `Vercel-CDN-Cache-Control` and `Cache-Control` to `s-maxage=31536000` and `public, max-age=31536000` respectively, caching the schema indefinitely until the next deployment.

```typescript
import { document } from "@/lib/openapi";
import { NextResponse } from "next/server";

export const dynamic = "force-static";

export function GET() {
  return NextResponse.json(document, {
    headers: {
      // cache indefinitely till next deployment
      "Vercel-CDN-Cache-Control": "s-maxage=31536000",
      "Cache-Control": "public, max-age=31536000",
    },
  });
}
```

Sources: [apps/web/app/api/route.ts:1-15](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/route.ts#L1-L15)

## Links API Path Definitions

### Overview

The links path definitions module aggregates individual OpenAPI operation objects for link management into a unified `ZodOpenApiPathsObject` structure. This collection covers endpoints for creating, listing, updating, deleting, counting, retrieving metadata, and performing bulk or upsert operations on workspace links. Each path mapping binds HTTP methods to specific operation objects configured with query schemas, response codes, tags, and security requirements.

Sources: [apps/web/lib/openapi/links/index.ts:1-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/index.ts#L1-L37)

### Path Map Structure

The `linksPaths` export maps route patterns to their respective HTTP method operations, defining endpoints under `/links`, `/links/count`, `/links/info`, `/links/{linkId}`, `/links/bulk`, and `/links/upsert`.

| Path Pattern | HTTP Method | Operation Object | Description / Action |
| :--- | :--- | :--- | :--- |
| `/links` | `post` | `createLink` | Create a new link |
| `/links` | `get` | `getLinks` | Retrieve a paginated list of links |
| `/links/count` | `get` | `getLinksCount` | Retrieve the total count of links |
| `/links/info` | `get` | `getLinkInfo` | Retrieve metadata/info for a specific link |
| `/links/{linkId}` | `patch` | `updateLink` | Update an existing link by identifier |
| `/links/{linkId}` | `delete` | `deleteLink` | Delete a link by identifier |
| `/links/bulk` | `post` | `bulkCreateLinks` | Bulk create multiple links |
| `/links/bulk` | `patch` | `bulkUpdateLinks` | Bulk update multiple links |
| `/links/bulk` | `delete` | `bulkDeleteLinks` | Bulk delete multiple links |
| `/links/upsert` | `put` | `upsertLink` | Upsert a link resource |

Sources: [apps/web/lib/openapi/links/index.ts:13-36](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/index.ts#L13-L36)

### Operation Configurations and Query Schemas

Individual operation objects configure request validation using Zod query schemas and declare response schemas alongside standard error responses and bearer token security.

```typescript
export const getLinks: ZodOpenApiOperationObject = {
  operationId: "getLinks",
  "x-speakeasy-name-override": "list",
  "x-speakeasy-pagination": {
    type: "offsetLimit",
    inputs: [
      { name: "page", in: "parameters", type: "page" },
      { name: "pageSize", in: "parameters", type: "limit" },
    ],
    outputs: { results: "$" },
  },
  summary: "List all links",
  description: "Retrieve a paginated list of links for the authenticated workspace.",
  requestParams: { query: getLinksQuerySchemaBase },
  responses: {
    "200": {
      description: "A list of links",
      content: { "application/json": { schema: z.array(LinkSchema) } },
    },
    ...openApiErrorResponses,
  },
  tags: ["Links"],
  security: [{ token: [] }],
};
```

Sources: [apps/web/lib/openapi/links/get-links.ts:6-46](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-links.ts#L6-L46)

Metadata retrieval and counting operations utilize specialized query schemas and response types:

- **`getLinkInfo`**: Mapped to `get` at `/links/info`, using `getLinkInfoQuerySchema` and returning a single `LinkSchema` object under a `200` status response. Speakeasy SDK generation is customized via `"x-speakeasy-name-override": "get"`.
- **`getLinksCount`**: Mapped to `get` at `/links/count`, using `getLinksCountQuerySchema` and returning a JSON number schema with a descriptive metadata annotation, overridden via `"x-speakeasy-name-override": "count"`.

```typescript
export const getLinkInfo: ZodOpenApiOperationObject = {
  operationId: "getLinkInfo",
  "x-speakeasy-name-override": "get",
  summary: "Retrieve a link",
  description: "Retrieve the info for a link.",
  requestParams: { query: getLinkInfoQuerySchema },
  responses: {
    "200": {
      description: "The retrieved link",
      content: { "application/json": { schema: LinkSchema } },
    },
    ...openApiErrorResponses,
  },
  tags: ["Links"],
  security: [{ token: [] }],
};
```

Sources: [apps/web/lib/openapi/links/get-link-info.ts:5-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-link-info.ts#L5-L26)

```typescript
export const getLinksCount: ZodOpenApiOperationObject = {
  operationId: "getLinksCount",
  "x-speakeasy-name-override": "count",
  summary: "Retrieve links count",
  description: "Retrieve the number of links for the authenticated workspace.",
  requestParams: { query: getLinksCountQuerySchema },
  responses: {
    "200": {
      description: "A list of links",
      content: {
        "application/json": {
          schema: z.number().meta({ description: "The number of links matching the query." }),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Links"],
  security: [{ token: [] }],
};
```

Sources: [apps/web/lib/openapi/links/get-links-count.ts:6-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/links/get-links-count.ts#L6-L29)

## Link Execution and Route Handlers

### Overview

Server-side REST route handlers for link management, metatags inspection, and iframe validation execute business logic, enforce rate limits and workspace permissions, and return typed responses. These handlers utilize Next.js App Router route conventions, Vercel edge runtimes, and upstream validation utilities.

Sources: [apps/web/app/api/links/route.ts:1-114](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L1-L114), [apps/web/app/api/links/metatags/route.ts:1-52](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts#L1-L52), [apps/web/app/api/links/iframeable/route.ts:1-27](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L1-L27)

### Link Management Handlers

The links route module provides `GET` and `POST` handlers wrapped with workspace authentication and permission checks. 

For retrieval (`GET`), `getLinksQuerySchemaExtended` parses search parameters, and `validateLinksQueryFilters` resolves folder IDs. Workspace limits dictate sorting and search behavior: if `workspace.totalLinks` exceeds `SORTABLE_LINKS_LIMIT` (10), sorting falls back to `"createdAt"`; if it exceeds `MEGA_WORKSPACE_LINKS_LIMIT` (100,000), search mode switches from `"fuzzy"` to `"exact"`.

Sources: [apps/web/app/api/links/route.ts:24-53](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L24-L53)

```typescript
export const GET = withWorkspace(
  async ({ headers, searchParams, workspace, session }) => {
    const filters = getLinksQuerySchemaExtended.parse(searchParams);

    const { folderIds } = await validateLinksQueryFilters({
      ...filters,
      workspace,
      sessionUserId: session.user.id,
    });

    const response = await getLinksForWorkspace({
      ...filters,
      workspaceId: workspace.id,
      folderIds,
      sortBy:
        workspace.totalLinks > SORTABLE_LINKS_LIMIT
          ? "createdAt"
          : filters.sortBy,
      searchMode:
        workspace.totalLinks > MEGA_WORKSPACE_LINKS_LIMIT ? "exact" : "fuzzy",
    });

    return NextResponse.json(response, {
      headers,
    });
  },
  {
    requiredPermissions: ["links.read"],
  },
);
```

Sources: [apps/web/app/api/links/route.ts:24-53](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L24-L53)

For creation (`POST`), usage limits are checked via `throwIfLinksUsageExceeded(workspace)`, and request bodies are parsed and validated using `createLinkBodySchemaAsync`. If the request is unauthenticated, an IP-based rate limit is asserted using `RATELIMIT_POLICIES.anonymousLinkCreate`. Links are processed via `processLink`, wrapped in a `DubApiError` if validation fails, and committed via `createLink`. Successful creations trigger an asynchronous background webhook via `waitUntil` and `sendWorkspaceWebhook`.

Sources: [apps/web/app/api/links/route.ts:56-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L56-L113)

```typescript
export const POST = withWorkspace(
  async ({ req, headers, session, workspace }) => {
    if (workspace) {
      throwIfLinksUsageExceeded(workspace);
    }

    const body = await createLinkBodySchemaAsync.parseAsync(
      await parseRequestBody(req),
    );

    if (!session) {
      const ip = req.headers.get("x-forwarded-for") || LOCALHOST_IP;
      await assertRateLimit({
        policy: RATELIMIT_POLICIES.anonymousLinkCreate,
        identifier: ip,
      });
    }

    const { link, error, code } = await processLink({
      payload: body,
      workspace,
      ...(session && { userId: session.user.id }),
    });

    if (error != null) {
      throw new DubApiError({
        code: code as ErrorCodes,
        message: error,
      });
    }

    try {
      const response = await createLink(link);

      if (response.projectId && response.userId) {
        waitUntil(
          sendWorkspaceWebhook({
            trigger: "link.created",
            workspace,
            data: linkEventSchema.parse(response),
          }),
        );
      }

      return NextResponse.json(response, {
        headers,
      });
    } catch (error) {
      throw new DubApiError({
        code: "unprocessable_entity",
        message: error.message,
      });
    }
  },
  {
    requiredPermissions: ["links.write"],
  },
);
```

Sources: [apps/web/app/api/links/route.ts:56-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L56-L113)

> [!WARNING]
> Unauthenticated link creation requests rely strictly on the client IP address (`x-forwarded-for` or `LOCALHOST_IP`) for anonymous rate limiting enforcement under `RATELIMIT_POLICIES.anonymousLinkCreate`.

Sources: [apps/web/app/api/links/route.ts:66-72](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L66-L72)

### Metatags and Iframe Validation Endpoints

Both the metatags and iframeable inspection endpoints execute on the Vercel edge runtime (`export const runtime = "edge"`).

Sources: [apps/web/app/api/links/metatags/route.ts:7](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts#L7), [apps/web/app/api/links/iframeable/route.ts:10](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L10)

The metatags endpoint validates request origins ending with `.dub.co` to assign CORS headers (`Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`), validates the target URL parameter against `getUrlQuerySchema`, enforces IP rate limits using `ratelimitOrThrow(req, "metatags")`, and fetches metadata via `getMetaTags(url)`. Responses include public caching directives (`Cache-Control: public, max-age=300`, `Vercel-CDN-Cache-Control: s-maxage=3600, stale-while-revalidate=86400`).

Sources: [apps/web/app/api/links/metatags/route.ts:9-51](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts#L9-L51)

```typescript
export async function GET(req: NextRequest) {
  try {
    const origin = req.headers.get("origin");
    const corsHeaders = {
      "Access-Control-Allow-Methods": "GET",
      "Access-Control-Allow-Headers": "Content-Type",
    };

    if (origin && origin.endsWith(".dub.co")) {
      corsHeaders["Access-Control-Allow-Origin"] = origin;
    }

    const { url } = getUrlQuerySchema.parse({
      url: req.nextUrl.searchParams.get("url"),
    });

    await ratelimitOrThrow(req, "metatags");

    const metatags = await getMetaTags(url);

    return NextResponse.json(
      {
        ...metatags,
        poweredBy: "Dub - The Modern Link Attribution Platform",
      },
      {
        headers: {
          ...corsHeaders,
          "Cache-Control": "public, max-age=300",
          "Vercel-CDN-Cache-Control":
            "s-maxage=3600, stale-while-revalidate=86400",
        },
      },
    );
  } catch (error) {
    return handleAndReturnErrorResponse(error);
  }
}
```

Sources: [apps/web/app/api/links/metatags/route.ts:9-51](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts#L9-L51)

The iframeable endpoint parses both URL and domain query parameters using combined Zod schemas (`getUrlQuerySchema.and(getDomainQuerySchema)`), enforces rate limits via `ratelimitOrThrow(req, "iframeable")`, and evaluates embedding permissions using `isIframeable({ url, requestDomain: domain })`.

Sources: [apps/web/app/api/links/iframeable/route.ts:12-26](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L12-L26)

```typescript
export async function GET(req: NextRequest) {
  try {
    const { url, domain } = getUrlQuerySchema
      .and(getDomainQuerySchema)
      .parse(getSearchParams(req.url));

    await ratelimitOrThrow(req, "iframeable");

    const iframeable = await isIframeable({ url, requestDomain: domain });

    return NextResponse.json({ iframeable });
  } catch (error) {
    return handleAndReturnErrorResponse(error);
  }
}
```

Sources: [apps/web/app/api/links/iframeable/route.ts:12-26](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L12-L26)

> [!NOTE]
> Edge-runtime route handlers such as metatags and iframe validation route unhandled errors directly to `handleAndReturnErrorResponse(error)` to format standardized JSON error payloads.

Sources: [apps/web/app/api/links/metatags/route.ts:48-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts#L48-L50), [apps/web/app/api/links/iframeable/route.ts:23-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L23-L25)

## Analytics and Tracking Specifications

### Overview

Dub exposes modular OpenAPI path definitions and tracking endpoints to query analytics and record conversion events, leads, and deep link opens. The analytics query path definition (`/analytics`) maps to the `retrieveAnalytics` operation, supporting parameterized queries across aggregate counts, timeseries, and various dimension breakdowns. Concurrently, the tracking namespace defines POST endpoints under `/track/lead`, `/track/sale`, and `/track/open` to record user interactions across mobile and web platforms.

Sources: [apps/web/lib/openapi/analytics/index.ts:7-110](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/analytics/index.ts#L7-L110), [apps/web/lib/openapi/track/index.ts:6-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/track/index.ts#L6-L16)

### Analytics Response Schema Union

The `retrieveAnalytics` OpenAPI operation defines a successful `200` response whose content schema is a Zod union covering fifteen distinct analytical data structures. These structures capture count aggregations, time-series metrics, geographical distributions, technical client metadata, and referrer information.

| Schema Identifier | Zod Definition Source | Description / Data Type |
| :--- | :--- | :--- |
| `AnalyticsCount` | `analyticsResponse.count` | Aggregate count metrics |
| `AnalyticsTimeseries` | `analyticsResponse.timeseries` | Array of timeseries data points |
| `AnalyticsContinents` | `analyticsResponse.continents` | Array of continent-level breakdowns |
| `AnalyticsCountries` | `analyticsResponse.countries` | Array of country-level breakdowns |
| `AnalyticsRegions` | `analyticsResponse.regions` | Array of region/state breakdowns |
| `AnalyticsCities` | `analyticsResponse.cities` | Array of city-level breakdowns |
| `AnalyticsDevices` | `analyticsResponse.devices` | Array of device type breakdowns |
| `AnalyticsBrowsers` | `analyticsResponse.browsers` | Array of browser breakdowns |
| `AnalyticsOS` | `analyticsResponse.os` | Array of operating system breakdowns |
| `AnalyticsTriggers` | `analyticsResponse.triggers` | Array of tracking trigger breakdowns |
| `AnalyticsEventNames` | `analyticsResponse.event_names` | Array of custom event name breakdowns |
| `AnalyticsReferers` | `analyticsResponse.referers` | Array of referer breakdowns |
| `AnalyticsRefererUrls` | `analyticsResponse.referer_urls` | Array of specific referer URL breakdowns |
| `AnalyticsTopLinks` | `analyticsResponse.top_links` | Array of top-performing links |
| `AnalyticsTopUrls` | `analyticsResponse.top_urls` | Array of top destination URLs |

Sources: [apps/web/lib/openapi/analytics/index.ts:22-95](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/analytics/index.ts#L22-L95)

> [!NOTE]
> The `retrieveAnalytics` operation enforces token-based security via `security: [{ token: [] }]` and overrides its Speakeasy SDK method name to `retrieve` using the `x-speakeasy-name-override` attribute.

Sources: [apps/web/lib/openapi/analytics/index.ts:9-10](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/analytics/index.ts#L9-L10), [apps/web/lib/openapi/analytics/index.ts:103](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/analytics/index.ts#L103)

### Deep Link Open Tracking Execution

The `/api/track/open` endpoint handles POST requests to track when a user opens an application via a Dub-powered deep link on iOS or Android. The operation is defined in OpenAPI via the `trackOpen` specification object, configured with `x-speakeasy-ignore: true` and validating request payloads against `trackOpenRequestSchema`.

Sources: [apps/web/lib/openapi/track/open.ts:8-33](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/track/open.ts#L8-L33), [apps/web/app/ee/api/track/open/route.ts:22-23](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/track/open/route.ts#L22-L23)

```mermaid
sequenceDiagram
    participant Client
    participant API as /api/track/open
    participant Redis as Redis Cache
    participant DB as Planetscale / Edge

    Client->>API: POST { deepLink, dubDomain }
    API->>API: Parse request body & compute identity hash
    alt deepLink is missing (Probabilistic tracking)
        API->>Redis: redis.scan(0, match: deepLinkClickCache:ip:domain:*)
        Redis-->>API: Return matching cache keys
        alt Cache hit
            API->>Redis: redis.get(cacheKey)
            Redis-->>API: DeepLinkClickData
            API-->>Client: 200 OK (cached click/link)
        else Cache miss
            API-->>Client: 200 OK (clickId: null, link: null)
        end
    else deepLink is present (Deterministic tracking)
        API->>API: Extract domain & key from URL
        par Redis Lookups
            API->>Redis: redisGlobalWithTimeout.get(clickCacheKey)
            API->>Redis: redisGlobalWithTimeout.get(linkCacheKey)
        end
        Redis-->>API: cachedClickId, cachedLink
        alt cachedLink missing
            API->>DB: getLinkViaEdge({ domain, key })
            DB-->>API: link record
            API->>Redis: linkCache.set(...) (via waitUntil)
        end
        alt cachedClickId missing
            API->>API: recordClick(...)
        end
        API-->>Client: 200 OK (clickId, linkData)
    end
```

Sources: [apps/web/app/ee/api/track/open/route.ts:31-163](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/track/open/route.ts#L31-L163)

The execution follows a strict sequence:
1. `trackOpenRequestSchema.parse()` validates the incoming JSON body containing `deepLink` and `dubDomain`.
2. `ipAddress(req)` or `LOCALHOST_IP` is resolved alongside `getIdentityHash(req)`.
3. If `deepLink` is omitted, the handler falls back to probabilistic IP-based tracking by scanning Upstash Redis for keys matching `deepLinkClickCache:${ip}:${dubDomain}:*`.
4. If `deepLink` is provided, the handler extracts the `domain` and `key`, then concurrently queries global Redis caches for existing click IDs and link metadata via `Promise.all`.
5. If the link is missing from the cache, it fetches the record via `getLinkViaEdge({ domain, key })`, formats it with `formatRedisLink()`, and asynchronously caches it using `waitUntil(linkCache.set(...))`.
6. If no cached click ID exists, a new `nanoid(16)` is assigned and `recordClick()` is invoked with `trigger: "deeplink"`.
7. Request completion and error flows trigger asynchronous audit logging via `captureRequestLog()` wrapped in `waitUntil()`.

Sources: [apps/web/app/ee/api/track/open/route.ts:31-198](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/track/open/route.ts#L31-L198)

> [!WARNING]
> If a request omits both `deepLink` and `dubDomain`, `trackOpenRequestSchema` invokes `.superRefine()` and throws a validation error requiring at least one parameter for deferred deep linking.

Sources: [apps/web/lib/zod/schemas/opens.ts:18-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/opens.ts#L18-L26)

## Domains and Custom Configuration

### Domains and Custom Configuration

The Dub REST API exposes domain management endpoints under `/domains`, supporting domain registration, custom domain creation, verification checks, and path configurations. These paths are declared modularly within `domainsPaths`, grouping the endpoints for listing, creating, patching, deleting, registering, and checking status.

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

| Path Pattern | HTTP Method | OpenAPI Object Ref | Purpose |
| :--- | :--- | :--- | :--- |
| `/domains` | `POST` | `createDomain` | Add and configure a new custom or `.dub.link` domain |
| `/domains` | `GET` | `listDomains` | Retrieve all domains associated with the workspace |
| `/domains/{slug}` | `PATCH` | `updateDomain` | Update domain settings, URLs, or metadata |
| `/domains/{slug}` | `DELETE` | `deleteDomain` | Delete or archive an existing domain |
| `/domains/register` | `POST` | `registerDomain` | Register a new domain through the provider |
| `/domains/status` | `GET` | `checkDomainStatus` | Check DNS and verification status for a domain |

Sources: [apps/web/lib/openapi/domains/index.ts:10-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/domains/index.ts#L10-L23)

### Domain Provisioning Execution Workflow

When a client creates a custom domain via a `POST /api/domains` request, the route handler executes a structured validation, provisioning, and persistence lifecycle.

Sources: [apps/web/app/api/domains/route.ts:97-173](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L97-L173)

The call-chain execution proceeds through these steps:
1. `parseRequestBody(req)` reads the raw HTTP request body, which is then validated and parsed asynchronously via `createDomainBodySchemaExtended.parseAsync(body)`.
2. Plan feature validation checks if the workspace plan is free and whether premium configuration flags (`logo`, `expiredUrl`, `notFoundUrl`, `assetLinks`, `appleAppSiteAssociation`, `deepviewData`) are present, throwing a `DubApiError` with code `forbidden` if violated.
3. JSON configurations for asset links, Apple App Site Association, and Deep View payloads are normalized via `parseDomainJsonConfig()`.
4. Domain validity and syntax are evaluated via `validateDomain(slug)`. If an error code is returned, it throws a `DubApiError`.
5. If the environment variable `process.env.VERCEL === "1"`, the domain is provisioned on Vercel infrastructure by calling `addDomainToVercel(slug)`. If Vercel returns an error other than `domain_already_in_use`, a `422` response is returned.
6. A unique identifier is generated via `createId({ prefix: "dom_" })`, and if a custom logo is provided, it is uploaded to storage via `storage.upload()`.
7. Inside a Prisma transaction (`prisma.$transaction`), subdomain constraints for `.dub.link` slugs and workspace domain limits are verified against `workspace.domainsLimit` before creating the final database record.

Sources: [apps/web/app/api/domains/route.ts:99-230](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L99-L230)

> [!CAUTION]
> Free-tier workspaces attempting to configure custom QR code logos, default expiration URLs, not found URLs, Asset Links, Apple App Site Association, or Deep View data will immediately receive a `forbidden` API error from the domain creation handler.

Sources: [apps/web/app/api/domains/route.ts:112-136](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L112-L136)

## Partners, Customers, and Ecosystem Endpoints

### Overview

The Dub REST API provides modular OpenAPI path definitions and operations for managing partners, program bounties, customers, embed tokens, and QR code generation. These path definitions aggregate separate operation schemas into cohesive OpenAPI path routers using `zod-openapi`.

Sources: [apps/web/lib/openapi/partners/index.ts:1-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/index.ts#L1-L32), [apps/web/lib/openapi/customers/index.ts:1-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/customers/index.ts#L1-L16)

### Partners and Bounties Endpoints

The partner program subsystem exposes routes for partner registration, link management, status actions, analytics, and bounty submissions. The analytics endpoint (`GET /partners/analytics`) supports polymorphic response types evaluated through `partnerAnalyticsQuerySchema`, returning either count records, timeseries arrays, or top links.

Sources: [apps/web/lib/openapi/partners/index.ts:11-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/index.ts#L11-L32), [apps/web/lib/openapi/partners/retrieve-analytics.ts:9-45](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/retrieve-analytics.ts#L9-L45), [apps/web/lib/openapi/bounties/list-bounty-submissions.ts:9-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/bounties/list-bounty-submissions.ts#L9-L37)

| Path Pattern | HTTP Method | OpenAPI Operation ID / Name Override | Purpose |
| :--- | :--- | :--- | :--- |
| `/partners` | `POST` | `createPartner` | Create a new partner |
| `/partners` | `GET` | `listPartners` | List all partners |
| `/partners/links` | `POST` | `createPartnerLink` | Create a partner link |
| `/partners/links` | `GET` | `retrievePartnerLinks` | Retrieve partner links |
| `/partners/links/upsert` | `PUT` | `upsertPartnerLink` | Upsert a partner link |
| `/partners/analytics` | `GET` | `retrievePartnerAnalytics` (`analytics`) | Retrieve partner analytics data |
| `/partners/ban` | `POST` | `banPartner` | Ban a partner |
| `/partners/deactivate` | `POST` | `deactivatePartner` | Deactivate a partner |

Sources: [apps/web/lib/openapi/partners/index.ts:11-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/index.ts#L11-L32), [apps/web/lib/openapi/partners/retrieve-analytics.ts:9-11](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/partners/retrieve-analytics.ts#L9-L11)

> [!NOTE]
> Bounty submission IDs on Dub are prefixed with `bnty_` and can be retrieved using the `listBountySubmissions` operation (`GET /bounties/{bountyId}/submissions`) with the `token` security scheme.

Sources: [apps/web/lib/openapi/bounties/list-bounty-submissions.ts:10-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/bounties/list-bounty-submissions.ts#L10-L37)

### Customers, Embed Tokens, and QR Codes

Customer management, referral embed tokens, and QR code generation paths map cleanly to dedicated router structures. The QR code endpoint (`GET /qr`) returns a raw PNG image response content type (`image/png`) validated via `getQRCodeQuerySchema`.

Sources: [apps/web/lib/openapi/customers/index.ts:7-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/customers/index.ts#L7-L16), [apps/web/lib/openapi/embed-tokens/index.ts:4-8](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/embed-tokens/index.ts#L4-L8), [apps/web/lib/openapi/qr/index.ts:7-33](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/qr/index.ts#L7-L33)

| Ecosystem Route Path | HTTP Method | Request Schema | Response Content Type / Schema |
| :--- | :--- | :--- | :--- |
| `/customers` | `GET` | `getCustomers` query schema | `application/json` (Array of customers) |
| `/customers/{id}` | `GET` | `getCustomer` path param | `application/json` (Customer object) |
| `/customers/{id}` | `PATCH` | `updateCustomer` body schema | `application/json` (Customer object) |
| `/customers/{id}` | `DELETE` | `deleteCustomer` path param | `application/json` (Deleted confirmation) |
| `/tokens/embed/referrals` | `POST` | Referrals embed body schema | `application/json` (Embed token) |
| `/qr` | `GET` | `getQRCodeQuerySchema` | `image/png` (String binary) |

Sources: [apps/web/lib/openapi/customers/index.ts:8-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/customers/index.ts#L8-L15), [apps/web/lib/openapi/embed-tokens/index.ts:5-7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/embed-tokens/index.ts#L5-L7), [apps/web/lib/openapi/qr/index.ts:12-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/qr/index.ts#L12-L23)

## Routing and Public API Middleware

### Overview

The Edge middleware architecture coordinates request dispatching, hostname recognition, and normalization across all public and application routes. Running on the Node.js runtime (`nodejs`), the entry point `middleware(req: NextRequest, ev: NextFetchEvent)` executes incoming requests by first invoking `parse(req)` to extract components such as `domain`, `path`, `key`, and `fullKey`. Axiom logging integrates directly into the request flow via `logger.info(...transformMiddlewareRequest(req))` followed by asynchronous flushing through `ev.waitUntil(logger.flush())`.

Sources: [apps/web/middleware.ts:1-40](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L1-L40)

### Request Routing and Middleware Dispatch Chain

Incoming requests are filtered through a central matcher config that intercepts all paths except API routes, Next.js internal paths (`_next/`), third-party proxy paths (`_proxy/`), and metadata files like `favicon.ico`, `sitemap.xml`, `robots.txt`, and `manifest.webmanifest`. 

Sources: [apps/web/middleware.ts:20-32](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L20-L32)

The dispatch flow executes in a deterministic conditional sequence:
1. `parse(req)` extracts request identifiers → 
2. `isAppHostname(domain)` routes to `AppMiddleware(req)` → 
3. `API_HOSTNAMES.has(domain)` routes to `ApiMiddleware(req)` → 
4. `path.startsWith("/stats/")` rewrites stats pages → 
5. `path.startsWith("/.well-known/")` rewrites supported files → 
6. `domain === "dub.sh" && DEFAULT_REDIRECTS[key]` redirects shortlinks → 
7. `ADMIN_HOSTNAMES.has(domain)` dispatches to `AdminMiddleware(req)` → 
8. `PARTNERS_HOSTNAMES.has(domain)` dispatches to `PartnersMiddleware(req)` → 
9. `isValidUrl(fullKey)` triggers `CreateLinkMiddleware(req)` → 
10. Fallback execution invokes `LinkMiddleware(req, ev)`.

Sources: [apps/web/middleware.ts:34-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L34-L89)

### API Middleware and Request Normalization

The `ApiMiddleware(req)` handler normalizes requests destined for API hostnames by parsing `fullPath` and evaluating specialized routing rules before forwarding requests to underlying Next.js API routes.

Sources: [apps/web/lib/middleware/api.ts:1-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts#L1-L16)

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

export function ApiMiddleware(req: NextRequest) {
  const { fullPath } = parse(req);

  // redirect to dub.co for /metatags
  if (fullPath.startsWith("/metatags")) {
    return NextResponse.redirect("https://dub.co", {
      status: 301,
    });
  }
  // Note: we don't have to account for paths starting with `/api`
  // since they're automatically excluded via our middleware matcher
  return NextResponse.rewrite(new URL(`/api${fullPath}`, req.url));
}
```

Sources: [apps/web/lib/middleware/api.ts:1-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts#L1-L16)

> [!WARNING]
> Paths starting with `/api` do not need explicit accounting inside `ApiMiddleware` because the root middleware matcher configuration explicitly excludes `/api/` routes from interception.

Sources: [apps/web/middleware.ts:25-31](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L25-L31), [apps/web/lib/middleware/api.ts:13-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts#L13-L15)

| Hostname or Path Pattern | Interception Condition | Middleware Handler / Action | Target Destination / Behavior |
| :--- | :--- | :--- | :--- |
| `app.dub.co` | `isAppHostname(domain)` | `AppMiddleware(req)` | Application dashboard routing |
| `api.dub.co` | `API_HOSTNAMES.has(domain)` | `ApiMiddleware(req)` | Rewrites to `/api${fullPath}` |
| `/stats/*` | `path.startsWith("/stats/")` | `NextResponse.rewrite` | Rewrites to `/[domain]/[key]/stats` |
| `/.well-known/*` | `path.startsWith("/.well-known/")` | `NextResponse.rewrite` | Rewrites to `/wellknown/[domain]/[file]` |
| `dub.sh` | `domain === "dub.sh" && DEFAULT_REDIRECTS[key]` | `NextResponse.redirect` | Redirects to default URL mapping |
| Admin Hostnames | `ADMIN_HOSTNAMES.has(domain)` | `AdminMiddleware(req)` | Administrative interface routing |
| Partner Hostnames | `PARTNERS_HOSTNAMES.has(domain)` | `PartnersMiddleware(req)` | Partner ecosystem routing |
| Valid URLs | `isValidUrl(fullKey)` | `CreateLinkMiddleware(req)` | On-the-fly link creation flow |

Sources: [apps/web/middleware.ts:41-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L41-L89), [apps/web/lib/middleware/api.ts:4-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts#L4-L16)

## Related

- [OAuth2 Provider and API Tokens](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/oauth2-provider-and-api-tokens)
- [Link Creation and Builder UI](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-creation-and-builder-ui)


## Sitemap

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