Skip to main content
doc0blade47/dubdocumentation home
Sign in

REST API

Last updated Oct 12, 202631% grounded
Relevant source files

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

Overview

Dub exposes a comprehensive programmatic interface centered around a robust OpenAPI specification, designed to power integrations, developer tooling, and automated link attribution workflows. The REST API is structured using Zod-backed schemas and Next.js route handlers that serve the complete OpenAPI definition at /api with long-term Vercel CDN caching.

Sources: apps/web/app/api/route.ts:1-13, apps/web/lib/openapi/index.ts:26-89

OpenAPI Specification Generation

Document Composition and Zod Integration

The public OpenAPI document is assembled using createDocument from the zod-openapi package, defining metadata for the Dub API. The document sets the OpenAPI version to 3.0.3 and includes metadata fields under info, such as title, description ("Dub is the modern link attribution platform for short links, conversion tracking, and affiliate programs."), version ("0.0.1"), contact information (support@dub.co), and the AGPL-3.0 license. The production server is configured with the base URL https://api.dub.co.

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",
    },
  ],

Sources: apps/web/lib/openapi/index.ts:26-48

The schema paths aggregate individual path modules covering link operations, analytics, events, tags, folders, domains, tracking, customers, partners, program applications, discount codes, commissions, payouts, embed tokens, QR codes, and bounties.

Path Module VariableDescription / Source Import
linksPathsLink CRUD and management paths
analyticsPathAnalytics telemetry paths
eventsPathEvent stream query paths
tagsPathsLink tag management paths
foldersPathsFolder grouping paths
domainsPathsCustom domain configuration paths
trackPathsConversion tracking paths
customersPathsCustomer entity paths
partnersPathsPartner program discovery paths
programApplicationsPathsAffiliate application paths
discountCodesPathsDiscount code management paths
commissionsPathsCommission tracking paths
payoutsPathsPartner payout paths
embedTokensPathsEmbeddable dashboard token paths
qrCodePathsQR Code Generator paths
bountiesPathsPartner bounty paths
import { analyticsPath } from "./analytics";
import { bountiesPaths } from "./bounties";
import { commissionsPaths } from "./commissions";
import { customersPaths } from "./customers";
import { discountCodesPaths } from "./discount-codes";
import { domainsPaths } from "./domains";
import { embedTokensPaths } from "./embed-tokens";
import { eventsPath } from "./events";
import { foldersPaths } from "./folders";
import { linksPaths } from "./links";
import { partnersPaths } from "./partners";
import { payoutsPaths } from "./payouts";
import { programApplicationsPaths } from "./program-applications";
import { qrCodePaths } from "./qr";
import { openApiErrorResponsesComponents } from "./responses";
import { tagsPaths } from "./tags";
import { trackPaths } from "./track";

Sources: apps/web/lib/openapi/index.ts:8-24, apps/web/lib/openapi/index.ts:49-66

Components registered within the OpenAPI specification define core data schemas, security schemes, and reusable error responses. The schema registry incorporates LinkSchema, LinkTagSchema, FolderSchema, DomainSchema, DiscountCodeSchema, webhookEventSchema, and LinkErrorSchema. Security is established via a bearer HTTP scheme named token with a Speakeasy example value of DUB_API_KEY. Reusable error responses are spread from openApiErrorResponsesComponents.

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:67-88

Note

The OpenAPI route handler exports a dynamic = "force-static" configuration directive, indicating that the specification is statically generated at build time rather than dynamically computed on each individual request. Sources: apps/web/app/api/route.ts:4-4

Route Handler and Caching

The generated OpenAPI document object is served via a Next.js GET route handler located in apps/web/app/api/route.ts. The handler returns the document as a JSON response using NextResponse.json(document), applying cache-control headers designed for long-term edge caching.

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:6-13

The response explicitly assigns cache headers to cache the specification at the edge until the next deployment:

  • Vercel-CDN-Cache-Control: s-maxage=31536000
  • Cache-Control: public, max-age=31536000

Sources: apps/web/app/api/route.ts:8-12

Workspace Authentication and Authorization

Request Authentication and Token Resolution

API requests targeting workspace resources are intercepted and authenticated by the withWorkspace wrapper middleware. The middleware inspects incoming HTTP requests for an Authorization header, enforcing that it carries the Bearer prefix before extracting the raw credential. When an authorization header is present but lacks the expected prefix, it raises a DubApiError with the bad_request code. Requests containing an API key starting with the dub_ prefix are classified as restricted tokens, whereas legacy or standard tokens are looked up against the database or verified via the local token cache using a hashed key representation.

const authorizationHeader = requestHeaders.get("Authorization");
if (authorizationHeader) {
  if (!authorizationHeader.startsWith("Bearer ")) {
    throw new DubApiError({
      code: "bad_request",
      message:
        "Misconfigured authorization header. Did you forget to add 'Bearer '? Learn more: https://d.to/auth",
    });
  }
  apiKey = authorizationHeader.replace("Bearer ", "");
}
 
let workspaceId: string | undefined;
let workspaceSlug: string | undefined;
let permissions: PermissionAction[] = [];
const isRestrictedToken = apiKey?.startsWith("dub_");
 

Sources: apps/web/lib/auth/workspace.ts:98-114

if (apiKey) {
  const hashedKey = await hashToken(apiKey);
 
  const cachedToken = await tokenCache.get({
    hashedKey,
  });
 
  if (!cachedToken) {
    const prismaArgs = {
      where: {
        hashedKey,
      },
      select: {
        id: true,
        expires: true,
        ...(isRestrictedToken && {
          scopes: true,
          projectId: true,
          installationId: true,
          project: {
            select: {
              plan: true,
              trialEndsAt: true,
            },
          },
        }),
        user: true,
      },
    };
 
    if (isRestrictedToken) {
      token = await prisma.restrictedToken.findUnique(prismaArgs);
    } else {
      token = await prisma.token.findUnique(prismaArgs);
    }
  }

Sources: apps/web/lib/auth/workspace.ts:168-203

If an API key is omitted, the middleware attempts to resolve an authenticated user session. Requests failing both token verification and session lookup trigger an unauthorized error response. Additionally, tokens that have passed their expiration timestamp are rejected with an access token expired message.

if (!token || !token.user) {
  throw new DubApiError({
    code: "unauthorized",
    message: "Unauthorized: Invalid API key.",
  });
}
 
if (token.expires && token.expires < new Date()) {
  throw new DubApiError({
    code: "unauthorized",
    message: "Unauthorized: Access token expired.",
  });
}

Sources: apps/web/lib/auth/workspace.ts:207-219, apps/web/lib/auth/workspace.ts:303-311

Warning

Omitting both an authorization header and a valid workspace identifier on non-anonymous routes will cause the middleware to throw either an unauthorized or not_found error, prompting clients to verify their workspaceId query parameters or upgrade from personal API keys. Sources: apps/web/lib/auth/workspace.ts:127-154

Workspace Resolution and Membership

Workspace targets are resolved from route parameters (params.idOrSlug, params.slug), query parameters (searchParams.workspaceId, searchParams.projectSlug), or directly from restricted token project associations (token.projectId). The identifier is inspected to determine whether it represents a workspace ID prefixed with ws_ or a standard workspace slug.

const idOrSlug =
  params?.idOrSlug ||
  searchParams.workspaceId ||
  params?.slug ||
  searchParams.projectSlug;
 
/*
  if there's no workspace ID or slug and it's not a restricted token:
  - special case for anonymous link creation
  - missing authorization header
  - user is still using personal API keys
*/
if (!idOrSlug && !isRestrictedToken) {
  // special case for anonymous link creation
  if (
    requestHeaders.has("dub-anonymous-link-creation") &&
    ["/links", "/api/links"].includes(req.nextUrl.pathname)
  ) {
    // @ts-expect-error
    return await handler({
      req: clonedReq,
      params,
      searchParams,
      headers: responseHeaders,
    });
    // missing authorization header
  } else if (!authorizationHeader) {
    throw new DubApiError({
      code: "unauthorized",
      message: "Missing Authorization header.",
    });
    // in case user is still using personal API keys
  } else {
    throw new DubApiError({
      code: "not_found",
      message:
        "Workspace ID not found. Did you forget to include a `workspaceId` query parameter? It looks like you might be using personal API keys, we also recommend refactoring to workspace API keys: https://d.to/keys",
    });
  }
}
 
if (idOrSlug) {
  if (idOrSlug.startsWith("ws_")) {
    workspaceId = normalizeWorkspaceId(idOrSlug);
  } else {
    workspaceSlug = idOrSlug;
  }
}

Sources: apps/web/lib/auth/workspace.ts:115-162, apps/web/lib/auth/workspace.ts:261-263

Once the workspace record is queried, the middleware verifies whether the authenticated user belongs to the workspace by checking the associated user relation and role assignments. Endpoints like GET /api/integrations can specify explicit permission requirements (such as workspaces.read) through options passed to withWorkspace. If the target workspace does not exist, or if the user holds no active membership, the system evaluates pending project invites by email lookup to return precise error codes.

export const GET = withWorkspace(
  async ({ workspace }) => {
    const integrations = await prisma.integration.findMany({
      where: {
        installations: {
          some: {
            project: {
              slug: workspace.slug,
            },
          },
        },
      },
      orderBy: {
        createdAt: "desc",
      },
    });
 
    return NextResponse.json(
      integrations.map((integration) =>
        installedIntegrationSchema.parse(integration),
      ),
    );
  },
  {
    requiredPermissions: ["workspaces.read"],
  },

Sources: apps/web/app/api/integrations/route.ts:7-32, apps/web/lib/auth/workspace.ts:335-363

Error CodeTrigger Condition
not_foundWorkspace ID or slug does not match any existing project record
invite_expiredUser is not a direct workspace member, but holds an expired project invite
invite_pendingUser is not a direct workspace member, but holds a valid pending project invite
// workspace exists but user is not part of it
if (workspace.users.length === 0) {
  const pendingInvites = await prisma.projectInvite.findUnique({
    where: {
      email_projectId: {
        email: session.user.email,
        projectId: workspace.id,
      },
    },
    select: {
      expires: true,
    },
  });
 
  // Clear so non-member denials are not attributed to this workspace
  // in API logs or Axiom error context.
  workspace = undefined;
 
  if (!pendingInvites) {
    throw new DubApiError({
      code: "not_found",
      message: "Workspace not found.",
    });
  } else if (pendingInvites.expires < new Date()) {
    throw new DubApiError({
      code: "invite_expired",
      message: "Workspace invite expired.",
    });
  } else {
    throw new DubApiError({
      code: "invite_pending",
      message: "Workspace invite pending.",
    });
  }
}

Sources: apps/web/lib/auth/workspace.ts:365-399

Note

When a user lacks direct membership but holds a valid pending invitation, the middleware clears the partial workspace reference before throwing the invite_pending error to prevent misattributing access denials in Axiom logs. Sources: apps/web/lib/auth/workspace.ts:379-398

Rate Limiting and Telemetry

Rate limits are enforced on every authenticated request based on the caller's plan tier or active session. Analytics and event endpoints evaluate request limits over a 1-second interval (1 s), whereas standard API operations evaluate limits over a 1-minute interval (1 m). Plan limits are retrieved dynamically using getRatelimitForPlan, incorporating trial period boundaries where applicable.

// Rate limit checks for API keys
let limit = 0;
let interval: `${number} s` | `${number} m` = isAnalytics
  ? "1 s"
  : "1 m";
 
const planLimit = getRatelimitForPlan(token.project?.plan || "free", {
  trialEndsAt: token.project?.trialEndsAt ?? null,
});
limit = planLimit.limits[isAnalytics ? "analyticsApi" : "api"];
 
const { success, headers } = await rateLimitRequest({
  identifier: `workspace:ratelimit:${hashedKey}`,
  requests: limit,
  interval,
});

Sources: apps/web/lib/auth/workspace.ts:230-245, apps/web/lib/auth/workspace.ts:313-322

When rate limits are exhausted, rateLimitRequest returns an unsuccessful status, causing the middleware to throw a rate_limit_exceeded error. Successful requests propagate rate limit metadata headers back to the client and asynchronously record token last-used timestamps throttled to once per minute.

const { success, headers } = await rateLimitRequest({
  identifier: `workspace:ratelimit:${hashedKey}`,
  requests: limit,
  interval,
});
 
if (headers) {
  for (const [key, value] of Object.entries(headers)) {
    responseHeaders.set(key, value);
  }
}
 
if (!success) {
  throw new DubApiError({
    code: "rate_limit_exceeded",
    message: "Too many requests.",
  });
}
 
// Find workspaceId if it's a restricted token
if (isRestrictedToken && token?.projectId) {
  workspaceId = token.projectId;
}
 
waitUntil(
  // update last used time for the token (only once every minute)
  (async () => {
    try {
      const { success } = await ratelimit(1, "1 m").limit(
        `last-used-${hashedKey}`,
      );
 
      if (success) {
        const prismaArgs = {
          where: {
            hashedKey,
          },
          data: {
            lastUsed: new Date(),
          },
        };
 
        if (isRestrictedToken) {
          await prisma.restrictedToken.update(prismaArgs);
        } else {
          await prisma.token.update(prismaArgs);
        }
      }
    } catch (error) {
      console.error(error);
    }
  })(),
);

Sources: apps/web/lib/auth/workspace.ts:241-293, apps/web/lib/auth/workspace.ts:323-332

Client Publishable Key Authentication

Publishable Key Validation and Request Flow

Public client routes authenticate incoming requests using the withPublishableKey wrapper function. The middleware extracts the Authorization header from the request, verifying that it starts with the Bearer prefix and contains a token beginning with the required dub_pk_ prefix. If either validation check fails, the handler throws a DubApiError with the bad_request code and a message indicating an invalid or missing publishable key.

const authorizationHeader = requestHeaders.get("Authorization");
if (authorizationHeader) {
  if (!authorizationHeader.startsWith("Bearer ")) {
    throw new DubApiError({
      code: "bad_request",
      message: "Invalid or missing publishable key.",
    });
  }
 
  const publishableKey = authorizationHeader.replace("Bearer ", "");
  if (!publishableKey.startsWith("dub_pk_")) {
    throw new DubApiError({
      code: "bad_request",
      message: "Invalid or missing publishable key.",
    });
  }

Sources: apps/web/lib/auth/publishable-key.ts:45-60

Once the key format is verified, the system enforces rate limiting using Upstash with a limit of 600 requests per minute (1 m) keyed directly by the publishable key string. Rate limit metadata headers (Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset) are injected into the response headers. If the rate limit is exceeded, the request is rejected with a rate_limit_exceeded error code.

const { success, limit, reset, remaining } = await ratelimit(
  600,
  "1 m",
).limit(publishableKey);
 
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/auth/publishable-key.ts:62-77

After rate limit evaluation, the workspace record is retrieved from the database by querying the publishableKey field. If no matching project is found, an unauthorized error is thrown. The workspace plan is normalized for business tiers before checking against the requiredPlan configuration.

workspace = await prisma.project.findUnique({
  where: {
    publishableKey,
  },
});
 
if (!workspace) {
  throw new DubApiError({
    code: "unauthorized",
    message: "Invalid publishable key.",
  });
}
 
const normalizedWorkspacePlan = [
  "business plus",
  "business max",
  "business extra",
].includes(workspace.plan)
  ? "business"
  : workspace.plan;
 
if (!requiredPlan.includes(normalizedWorkspacePlan)) {
  throw new DubApiError({
    code: "forbidden",
    message: "Unauthorized: Need higher plan.",
  });
}

Sources: apps/web/lib/auth/publishable-key.ts:79-105

Note

Workspace plans matching business plus, business max, or business extra are automatically normalized to business before validating against the endpoint's requiredPlan array. Sources: apps/web/lib/auth/publishable-key.ts:92-99

Error Handling and Telemetry Capture

When request handlers execute successfully or encounter errors, the middleware records request logs asynchronously using Vercel's waitUntil combined with captureRequestLog and Axiom integration.

waitUntil(
  captureRequestLog({
    req: reqForLog,
    response,
    workspace,
    session: undefined,
    token: null,
    url,
    requestHeaders,
    startTime,
  }),
);

Sources: apps/web/lib/auth/publishable-key.ts:115-126

Error CodeTrigger Condition
bad_requestAuthorization header is missing, does not start with Bearer , or the token does not start with dub_pk_
rate_limit_exceededRequest volume exceeds 600 requests per minute for the given publishable key
unauthorizedAuthorization header is completely absent, or no workspace matches the provided publishable key
forbiddenThe normalized workspace plan is not included in the endpoint's requiredPlan array
const authorizationHeader = requestHeaders.get("Authorization");
if (authorizationHeader) {
  if (!authorizationHeader.startsWith("Bearer ")) {
    throw new DubApiError({
      code: "bad_request",
      message: "Invalid or missing publishable key.",
    });
  }
 
  const publishableKey = authorizationHeader.replace("Bearer ", "");
  if (!publishableKey.startsWith("dub_pk_")) {
    throw new DubApiError({
      code: "bad_request",
      message: "Invalid or missing publishable key.",
    });
  }
 
  const { success, limit, reset, remaining } = await ratelimit(
    600,
    "1 m",
  ).limit(publishableKey);
 
  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.",
    });
  }
 
  workspace = await prisma.project.findUnique({
    where: {
      publishableKey,
    },
  });
 
  if (!workspace) {
    throw new DubApiError({
      code: "unauthorized",
      message: "Invalid publishable key.",
    });
  }
 
  const normalizedWorkspacePlan = [
    "business plus",
    "business max",
    "business extra",
  ].includes(workspace.plan)
    ? "business"
    : workspace.plan;
 
  if (!requiredPlan.includes(normalizedWorkspacePlan)) {
    throw new DubApiError({
      code: "forbidden",
      message: "Unauthorized: Need higher plan.",
    });
  }

Sources: apps/web/lib/auth/publishable-key.ts:45-105, apps/web/lib/auth/publishable-key.ts:130-134

Warning

If an exception occurs during request processing after a workspace has been successfully resolved, the error response is still captured in the telemetry logs alongside the valid workspace reference. Sources: apps/web/lib/auth/publishable-key.ts:135-154

Route Definitions and Query Operations

Link routing operations are structured under the /links path tree using ZodOpenApiPathsObject definitions. The resource map covers individual link mutations, bulk procedures, and counting endpoints alongside standard query retrieval.

export const linksPaths: ZodOpenApiPathsObject = {
  "/links": {
    post: createLink,
    get: getLinks,
  },
  "/links/count": {
    get: getLinksCount,
  },
  "/links/info": {
    get: getLinkInfo,
  },
  "/links/{linkId}": {
    patch: updateLink,
    delete: deleteLink,
  },
  "/links/bulk": {
    post: bulkCreateLinks,
    patch: bulkUpdateLinks,
    delete: bulkDeleteLinks,
  },
  "/links/upsert": {
    put: upsertLink,
  },
};

Sources: apps/web/lib/openapi/links/index.ts:13-36

The getLinks operation defines an OpenAPI object with operationId set to getLinks, a Speakeasy name override of list, and offset-limit pagination mapping parameters page and pageSize to input parameters while outputting results directly from root. It validates incoming request query parameters using getLinksQuerySchemaBase and responds with a 200 array of LinkSchema items.

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

Metadata retrieval for single links is handled by getLinkInfo, exposing the getLinkInfo operation ID and get name override. It accepts query parameters validated via getLinkInfoQuerySchema and returns a single LinkSchema object under a 200 status code response.

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

Count aggregation is handled through the getLinksCount operation defined at /links/count. It utilizes getLinksCountQuerySchema for query validation and returns a numerical Zod schema carrying metadata describing the matching record count.

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

Operation IDMethod & PathSpeakeasy OverrideRequest Query SchemaResponse Schema
getLinksGET /linkslistgetLinksQuerySchemaBasez.array(LinkSchema)
getLinkInfoGET /links/infogetgetLinkInfoQuerySchemaLinkSchema
getLinksCountGET /links/countcountgetLinksCountQuerySchemaz.number()
"/links/count": {
  get: getLinksCount,
},
"/links/info": {
  get: getLinkInfo,
},

Sources: apps/web/lib/openapi/links/index.ts:18-23, apps/web/lib/openapi/links/get-links.ts:30-41, apps/web/lib/openapi/links/get-link-info.ts:10-21, apps/web/lib/openapi/links/get-links-count.ts:11-24

Note

All link query operations require token security credentials and inherit standard OpenAPI error responses via openApiErrorResponses. Sources: apps/web/lib/openapi/links/get-links.ts:41-45, apps/web/lib/openapi/links/get-link-info.ts:22-25, apps/web/lib/openapi/links/get-links-count.ts:25-28

Analytics and Event Telemetry

Analytics Endpoint and Response Schema

The analytics API is mounted under the /analytics route path and handles retrieval for links, domains, or workspaces based on query configuration. The retrieveAnalytics operation defines its query parameters using analyticsQuerySchema and uses a Speakeasy name override of retrieve.

const retrieveAnalytics: ZodOpenApiOperationObject = {
  operationId: "retrieveAnalytics",
  "x-speakeasy-name-override": "retrieve",
  summary:
    "Retrieve analytics for a link, a domain, or the authenticated workspace.",
  description:
    "Retrieve analytics for a link, a domain, or the authenticated workspace. The response type depends on the `event` and `type` query parameters.",
  requestParams: {
    query: analyticsQuerySchema,
  },

Sources: apps/web/lib/openapi/analytics/index.ts:7-16

The response schema for retrieveAnalytics accepts a union of types depending on the requested dimension, validating outputs against specific metric definitions. Success responses return application JSON formatted as either an analytics count or an array of dimension objects.

OpenAPI IDZod Schema BindingTarget Metric Dimension
AnalyticsCountanalyticsResponse.countAggregate count metrics
AnalyticsTimeseriesanalyticsResponse.timeseriesTimeseries datapoints
AnalyticsContinentsanalyticsResponse.continentsContinents breakdown
AnalyticsCountriesanalyticsResponse.countriesCountries breakdown
AnalyticsRegionsanalyticsResponse.regionsRegions breakdown
AnalyticsCitiesanalyticsResponse.citiesCities breakdown
AnalyticsDevicesanalyticsResponse.devicesDevice types
AnalyticsBrowsersanalyticsResponse.browsersBrowsers breakdown
AnalyticsOSanalyticsResponse.osOperating systems
AnalyticsTriggersanalyticsResponse.triggersClick triggers
AnalyticsEventNamesanalyticsResponse.event_namesEvent names
AnalyticsReferersanalyticsResponse.referersReferers breakdown
AnalyticsRefererUrlsanalyticsResponse.referer_urlsReferer URLs
AnalyticsTopLinksanalyticsResponse.top_linksTop performing links
AnalyticsTopUrlsanalyticsResponse.top_urlsTop target URLs
"200": {
  description: "Analytics data",
  content: {
    "application/json": {
      schema: z.union([
        analyticsResponse.count.meta({
          id: "AnalyticsCount",
        }),
        z.array(
          analyticsResponse.timeseries.meta({
            id: "AnalyticsTimeseries",
          }),
        ),
        z.array(
          analyticsResponse.continents.meta({
            id: "AnalyticsContinents",
          }),
        ),
        z.array(
          analyticsResponse.countries.meta({
            id: "AnalyticsCountries",
          }),
        ),
        z.array(
          analyticsResponse.regions.meta({
            id: "AnalyticsRegions",
          }),
        ),
        z.array(
          analyticsResponse.cities.meta({
            id: "AnalyticsCities",
          }),
        ),
        z.array(
          analyticsResponse.devices.meta({
            id: "AnalyticsDevices",
          }),
        ),
        z.array(
          analyticsResponse.browsers.meta({
            id: "AnalyticsBrowsers",
          }),
        ),
        z.array(
          analyticsResponse.os.meta({
            id: "AnalyticsOS",
          }),
        ),
        z.array(
          analyticsResponse.triggers.meta({
            id: "AnalyticsTriggers",
          }),
        ),
        z.array(
          analyticsResponse.event_names.meta({
            id: "AnalyticsEventNames",
          }),
        ),
        z.array(
          analyticsResponse.referers.meta({
            id: "AnalyticsReferers",
          }),
        ),
        z.array(
          analyticsResponse.referer_urls.meta({
            id: "AnalyticsRefererUrls",
          }),
        ),
        z.array(
          analyticsResponse.top_links.meta({
            id: "AnalyticsTopLinks",
          }),
        ),
        z.array(
          analyticsResponse.top_urls.meta({
            id: "AnalyticsTopUrls",
          }),
        ),
      ]),
    },

Sources: apps/web/lib/openapi/analytics/index.ts:18-97

Note

The retrieveAnalytics operation is tagged under Analytics, requires bearer token security credentials via [{ token: [] }], and inherits standard OpenAPI error responses. Sources: apps/web/lib/openapi/analytics/index.ts:100-104

Raw Event Streaming and Schemas

Event telemetry retrieval is exposed via the /events endpoint through the listEvents operation. It accepts paginated query parameters defined by eventsQuerySchema and returns a list of telemetry events for the authenticated workspace.

export const listEvents: ZodOpenApiOperationObject = {
  operationId: "listEvents",
  "x-speakeasy-name-override": "list",
  summary: "List all events",
  description:
    "Retrieve a paginated list of events for the authenticated workspace.",
  requestParams: {
    query: eventsQuerySchema,
  },

Sources: apps/web/lib/openapi/events/index.ts:9-17

The response content for listEvents validates items against a discriminated union keyed on the event property. This union supports click, lead, and sale event response schemas.

responses: {
  "200": {
    description: "A list of events",
    content: {
      "application/json": {
        schema: z.array(
          z.discriminatedUnion("event", [
            clickEventResponseSchema,
            leadEventResponseSchema,
            saleEventResponseSchema,
          ]),
        ),
      },
    },
  },

Sources: apps/web/lib/openapi/events/index.ts:18-32

Schema ConstantResponse TypeDiscriminator Value
clickEventResponseSchemaClick telemetry eventclick
leadEventResponseSchemaLead conversion eventlead
saleEventResponseSchemaSale conversion eventsale
schema: z.array(
  z.discriminatedUnion("event", [
    clickEventResponseSchema,
    leadEventResponseSchema,
    saleEventResponseSchema,
  ]),

Sources: apps/web/lib/openapi/events/index.ts:23-28

Warning

Omitting the discriminator event parameter during event payload consumption will cause Zod validation failures against the discriminated union schema. Sources: apps/web/lib/openapi/events/index.ts:23-28

Domain and Organization Resource Management

Workspace Domain and Entity Query Operations

The OpenAPI specification defines resource endpoints to query and manage organization-level assets including domains, tags, and folders. The listDomains operation is exposed under the Domains tag with operation ID listDomains and utilizes an offset-limit pagination schema.

export const listDomains: ZodOpenApiOperationObject = {
  operationId: "listDomains",
  "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: "$",
    },
  },

Sources: apps/web/lib/openapi/domains/list-domains.ts:6-26

Query inputs accept page and pageSize parameters mapped to parameters input type, returning an array of DomainSchema objects with standard error responses and token security requirements.

summary: "List all domains",
description:
  "Retrieve a paginated list of domains for the authenticated workspace.",
requestParams: {
  query: getDomainsQuerySchema,
},
responses: {
  "200": {
    description: "The domains were retrieved.",
    content: {
      "application/json": {
        schema: z.array(DomainSchema),
      },
    },
  },
  ...openApiErrorResponses,
},
tags: ["Domains"],
security: [{ token: [] }],

Sources: apps/web/lib/openapi/domains/list-domains.ts:27-45

Note

The Speakeasy configuration for listDomains defines pagination outputs using the root path output $. Sources: apps/web/lib/openapi/domains/list-domains.ts:23-26

Tags and Folders Management Schemas

Organization categorization assets are handled through dedicated resource definitions. The getTags operation retrieves paginated tags for the authenticated workspace using getTagsQuerySchema and validates response items against LinkTagSchema.

export const getTags: ZodOpenApiOperationObject = {
  operationId: "getTags",
  "x-speakeasy-name-override": "list",
  summary: "List all tags",
  description:
    "Retrieve a paginated list of tags for the authenticated workspace.",
  requestParams: {
    query: getTagsQuerySchema,
  },
  responses: {
    "200": {
      description: "A list of tags",
      content: {
        "application/json": {
          schema: z.array(LinkTagSchema),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Tags"],
  security: [{ token: [] }],

Sources: apps/web/lib/openapi/tags/get-tags.ts:6-27

Tag mutation paths are grouped under /tags and /tags/{id} routes, combining creation, listing, updating, and deletion behaviors.

export const tagsPaths: ZodOpenApiPathsObject = {
  "/tags": {
    post: createTag,
    get: getTags,
  },
  "/tags/{id}": {
    patch: updateTag,
    delete: deleteTag,
  },
};

Sources: apps/web/lib/openapi/tags/index.ts:8-17

Similarly, folder retrieval is exposed via the listFolders operation under the Folders tag, validating query parameters with listFoldersQuerySchema and returning arrays matching FolderSchema.

export const listFolders: ZodOpenApiOperationObject = {
  operationId: "listFolders",
  "x-speakeasy-name-override": "list",
  summary: "List all folders",
  description:
    "Retrieve a paginated list of folders for the authenticated workspace.",
  requestParams: {
    query: listFoldersQuerySchema,
  },
  responses: {
    "200": {
      description: "A list of folders",
      content: {
        "application/json": {
          schema: z.array(FolderSchema),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Folders"],
  security: [{ token: [] }],

Sources: apps/web/lib/openapi/folders/list-folders.ts:9-30

Operation IDHTTP Method & PathQuery Schema BindingResponse SchemaTag
listDomainsGET /domainsgetDomainsQuerySchemaz.array(DomainSchema)Domains
getTagsGET /tagsgetTagsQuerySchemaz.array(LinkTagSchema)Tags
listFoldersGET /folderslistFoldersQuerySchemaz.array(FolderSchema)Folders
requestParams: {
  query: getDomainsQuerySchema,
},
responses: {
  "200": {
    description: "The domains were retrieved.",
    content: {
      "application/json": {
        schema: z.array(DomainSchema),
      },
    },
  },
…
requestParams: {
  query: getTagsQuerySchema,
},
responses: {
  "200": {
    description: "A list of tags",
    content: {
      "application/json": {
        schema: z.array(LinkTagSchema),
      },
…
requestParams: {
  query: listFoldersQuerySchema,
},
responses: {
  "200": {
    description: "A list of folders",
    content: {
      "application/json": {
        schema: z.array(FolderSchema),
      },

Sources: apps/web/lib/openapi/domains/list-domains.ts:30-41, apps/web/lib/openapi/tags/get-tags.ts:12-21, apps/web/lib/openapi/folders/list-folders.ts:15-24

Warning

All resource listing endpoints require bearer token workspace authorization (security: [{ token: [] }]) and will return OpenAPI error responses if credentials are omitted or invalid. Sources: apps/web/lib/openapi/domains/list-domains.ts:42-45, apps/web/lib/openapi/tags/get-tags.ts:24-27, apps/web/lib/openapi/folders/list-folders.ts:27-30

Partner and Commerce Resource Endpoints

Customer Entity Endpoints

Customer management operations are exposed via paths under /customers and /customers/{id}. The getCustomers operation retrieves a paginated list of customers for the authenticated workspace, utilizing cursor pagination via Speakeasy annotations with the startingAfter cursor input parameter and returning an array of CustomerEnrichedSchema objects.

export const getCustomers: ZodOpenApiOperationObject = {
  operationId: "getCustomers",
  "x-speakeasy-name-override": "list",
  "x-speakeasy-pagination": {
    type: "cursor",
    inputs: [
      {
        name: "startingAfter",
        in: "parameters",
        type: "cursor",
      },
    ],
    outputs: {
      nextCursor: "$[-1].id",
    },
  },
  summary: "List all customers",
  description:
    "Retrieve a paginated list of customers for the authenticated workspace.",
  requestParams: {
    query: getCustomersQuerySchema,
  },
  responses: {
    "200": {
      description: "The list of customers.",
      content: {
        "application/json": {
          schema: z.array(CustomerEnrichedSchema),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Customers"],
  security: [{ token: [] }],
};

Sources: apps/web/lib/openapi/customers/get-customers.ts:9-44

Individual customer retrieval is handled by getCustomer, which maps path parameters to CustomerEnrichedSchema.pick({ id: true }) and query parameters to expanded fields selection. To retrieve a customer by an external ID, callers must prefix the identifier value with ext_.

export const getCustomer: ZodOpenApiOperationObject = {
  operationId: "getCustomer",
  "x-speakeasy-name-override": "get",
  summary: "Retrieve a customer",
  description:
    "Retrieve a customer by ID for the authenticated workspace. To retrieve a customer by external ID, prefix the ID with `ext_`.",
  requestParams: {
    path: CustomerEnrichedSchema.pick({ id: true }),
    query: getCustomersQuerySchema.pick({ includeExpandedFields: true }),
  },
  responses: {
    "200": {
      description: "The customer object.",
      content: {
        "application/json": {
          schema: CustomerEnrichedSchema,
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Customers"],
  security: [{ token: [] }],
};

Sources: apps/web/lib/openapi/customers/get-customer.ts:8-31

Note

The getCustomer operation overrides Speakeasy's method naming to get while getCustomers maps to list, supporting distinct SDK client generation names across the Customers tag group. Sources: apps/web/lib/openapi/customers/get-customers.ts:10-11, apps/web/lib/openapi/customers/get-customer.ts:9-10

Discount Code Schemas and Paths

Discount codes are defined under /discount-codes and /discount-codes/{idOrCode} endpoints. The listDiscountCodes operation permits querying discount codes within a program or filtered by specific attributes using getDiscountCodesQuerySchema, returning an array matching DiscountCodeSchema.

export const listDiscountCodes: ZodOpenApiOperationObject = {
  operationId: "listDiscountCodes",
  "x-speakeasy-name-override": "list",
  summary: "List discount codes",
  description:
    "Retrieve a paginated list of discount codes in a program or filtered by partner, discount, or code.",
  requestParams: {
    query: getDiscountCodesQuerySchema,
  },
  responses: {
    "200": {
      description: "The list of discount codes.",
      content: {
        "application/json": {
          schema: z.array(DiscountCodeSchema),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Discount Codes"],
  security: [{ token: [] }],
};

Sources: apps/web/lib/openapi/discount-codes/list-discount-codes.ts:9-31

The discount code router aggregates listing, creation, and deletion routes into a single discountCodesPaths object configuration.

export const discountCodesPaths: ZodOpenApiPathsObject = {
  "/discount-codes": {
    get: listDiscountCodes,
    post: createDiscountCode,
  },
  "/discount-codes/{idOrCode}": {
    delete: deleteDiscountCode,
  },
};

Sources: apps/web/lib/openapi/discount-codes/index.ts:6-14

Operation IDHTTP Method & PathPrimary SchemaTag
getCustomersGET /customersCustomerEnrichedSchemaCustomers
getCustomerGET /customers/{id}CustomerEnrichedSchemaCustomers
listDiscountCodesGET /discount-codesDiscountCodeSchemaDiscount Codes
retrievePartnerLinksGET /partners/linksProgramPartnerLinkSchemaPartners
summary: "List all customers",
description:
  "Retrieve a paginated list of customers for the authenticated workspace.",
requestParams: {
  query: getCustomersQuerySchema,
},
responses: {
  "200": {
    description: "The list of customers.",
    content: {
      "application/json": {
        schema: z.array(CustomerEnrichedSchema),
      },
    },
  },
  ...openApiErrorResponses,
},
tags: ["Customers"],
…
summary: "Retrieve a customer",
description:
  "Retrieve a customer by ID for the authenticated workspace. To retrieve a customer by external ID, prefix the ID with `ext_`.",
requestParams: {
  path: CustomerEnrichedSchema.pick({ id: true }),
  query: getCustomersQuerySchema.pick({ includeExpandedFields: true }),
},
responses: {
  "200": {
    description: "The customer object.",
    content: {
      "application/json": {
        schema: CustomerEnrichedSchema,
      },
    },
  },
  ...openApiErrorResponses,
},
tags: ["Customers"],
…
summary: "List discount codes",
description:
  "Retrieve a paginated list of discount codes in a program or filtered by partner, discount, or code.",
requestParams: {
  query: getDiscountCodesQuerySchema,
},
responses: {
  "200": {
    description: "The list of discount codes.",
    content: {
      "application/json": {
        schema: z.array(DiscountCodeSchema),
      },
    },
  },
  ...openApiErrorResponses,
},
tags: ["Discount Codes"],
…
summary: "Retrieve a partner's links.",
description: "Retrieve a partner's links by their partner ID or tenant ID.",
requestParams: {
  query: retrievePartnerLinksSchema,
},
responses: {
  "200": {
    description: "The retrieved partner links.",
    content: {
      "application/json": {
        schema: z.array(ProgramPartnerLinkSchema),
      },
    },
  },
  ...openApiErrorResponses,
},
tags: ["Partners"],

Sources: apps/web/lib/openapi/customers/get-customers.ts:25-42, apps/web/lib/openapi/customers/get-customer.ts:11-29, apps/web/lib/openapi/discount-codes/list-discount-codes.ts:12-29, apps/web/lib/openapi/partners/retrieve-partner-links.ts:10-26

Partner Program Telemetry and Analytics

Partner program telemetry encompasses partner link retrieval, analytics, and program applications. The retrievePartnerLinks operation queries partner links by partner ID or tenant ID using retrievePartnerLinksSchema and returns a list of ProgramPartnerLinkSchema items.

export const retrievePartnerLinks: ZodOpenApiOperationObject = {
  operationId: "retrievePartnerLinks",
  "x-speakeasy-name-override": "retrieveLinks",
  summary: "Retrieve a partner's links.",
  description: "Retrieve a partner's links by their partner ID or tenant ID.",
  requestParams: {
    query: retrievePartnerLinksSchema,
  },
  responses: {
    "200": {
      description: "The retrieved partner links.",
      content: {
        "application/json": {
          schema: z.array(ProgramPartnerLinkSchema),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Partners"],
  security: [{ token: [] }],
};

Sources: apps/web/lib/openapi/partners/retrieve-partner-links.ts:7-28

Analytics for partners within a program are retrieved via retrievePartnerAnalytics, which validates query parameters against partnerAnalyticsQuerySchema. The response schema varies via a Zod union supporting count metadata, timeseries arrays, and top-links metrics based on the groupBy query parameter.

export const retrievePartnerAnalytics: ZodOpenApiOperationObject = {
  operationId: "retrievePartnerAnalytics",
  "x-speakeasy-name-override": "analytics",
  summary: "Retrieve analytics for a partner",
  description:
    "Retrieve analytics for a partner within a program. The response type vary based on the `groupBy` query parameter.",
  requestParams: {
    query: partnerAnalyticsQuerySchema,
  },
  responses: {
    "200": {
      description: "Partner analytics data",
      content: {
        "application/json": {
          schema: z.union([
            partnerAnalyticsResponseSchema.count.meta({
              id: "PartnerAnalyticsCount",
            }),
            z.array(
              partnerAnalyticsResponseSchema.timeseries.meta({
                id: "PartnerAnalyticsTimeseries",
              }),
            ),
            z.array(
              partnerAnalyticsResponseSchema.top_links.meta({
                id: "PartnerAnalyticsTopLinks",
              }),
            ),
          ]),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Partners"],
  security: [{ token: [] }],
};

Sources: apps/web/lib/openapi/partners/retrieve-analytics.ts:9-45

Program applications route handles listing applications alongside approval and rejection endpoints under /program-applications, /program-applications/approve, and /program-applications/reject.

export const programApplicationsPaths: ZodOpenApiPathsObject = {
  "/program-applications": {
    get: listProgramApplications,
  },
  "/program-applications/approve": {
    post: approveProgramApplication,
  },
  "/program-applications/reject": {
    post: rejectProgramApplication,
  },
};

Sources: apps/web/lib/openapi/program-applications/index.ts:6-16

Warning

Partner program summary route handlers are protected with withPartnerProfile middleware to ensure authenticated partner context is injected before marketplace telemetry fetchers execute. Sources: apps/web/app/(ee)/api/network/programs/summary/route.ts:1-10

Was this page helpful?