Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/app/api/route.ts
- apps/web/lib/openapi/links/get-links.ts
- apps/web/lib/openapi/tags/get-tags.ts
- apps/web/lib/openapi/analytics/index.ts
- apps/web/app/api/oauth/userinfo/route.ts
- apps/web/lib/openapi/customers/get-customers.ts
- apps/web/lib/openapi/folders/list-folders.ts
- apps/web/lib/openapi/links/get-link-info.ts
- apps/web/lib/openapi/qr/index.ts
- apps/web/lib/auth/workspace.ts
- apps/web/lib/openapi/embed-tokens/index.ts
- apps/web/lib/openapi/index.ts
- apps/web/lib/auth/publishable-key.ts
- apps/web/lib/openapi/domains/list-domains.ts
- apps/web/app/api/logs/route.ts
- apps/web/lib/openapi/discount-codes/index.ts
- apps/web/app/ee/api/programs/programId/resources/route.ts
- apps/web/app/ee/api/marketplace/programs/route.ts
- apps/web/lib/openapi/partners/retrieve-partner-links.ts
- apps/web/app/ee/api/network/programs/summary/route.ts
- apps/web/lib/openapi/customers/index.ts
- apps/web/lib/openapi/discount-codes/list-discount-codes.ts
- apps/web/lib/openapi/customers/get-customer.ts
- apps/web/lib/openapi/tags/index.ts
- apps/web/app/api/integrations/route.ts
- apps/web/lib/openapi/links/index.ts
- apps/web/lib/openapi/links/get-links-count.ts
- apps/web/lib/openapi/partners/retrieve-analytics.ts
- apps/web/lib/openapi/events/index.ts
- apps/web/lib/openapi/program-applications/index.ts
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 Variable | Description / Source Import |
|---|---|
linksPaths | Link CRUD and management paths |
analyticsPath | Analytics telemetry paths |
eventsPath | Event stream query paths |
tagsPaths | Link tag management paths |
foldersPaths | Folder grouping paths |
domainsPaths | Custom domain configuration paths |
trackPaths | Conversion tracking paths |
customersPaths | Customer entity paths |
partnersPaths | Partner program discovery paths |
programApplicationsPaths | Affiliate application paths |
discountCodesPaths | Discount code management paths |
commissionsPaths | Commission tracking paths |
payoutsPaths | Partner payout paths |
embedTokensPaths | Embeddable dashboard token paths |
qrCodePaths | QR Code Generator paths |
bountiesPaths | Partner 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=31536000Cache-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 Code | Trigger Condition |
|---|---|
not_found | Workspace ID or slug does not match any existing project record |
invite_expired | User is not a direct workspace member, but holds an expired project invite |
invite_pending | User 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 Code | Trigger Condition |
|---|---|
bad_request | Authorization header is missing, does not start with Bearer , or the token does not start with dub_pk_ |
rate_limit_exceeded | Request volume exceeds 600 requests per minute for the given publishable key |
unauthorized | Authorization header is completely absent, or no workspace matches the provided publishable key |
forbidden | The 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
Link Operations and Schemas
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 ID | Method & Path | Speakeasy Override | Request Query Schema | Response Schema |
|---|---|---|---|---|
getLinks | GET /links | list | getLinksQuerySchemaBase | z.array(LinkSchema) |
getLinkInfo | GET /links/info | get | getLinkInfoQuerySchema | LinkSchema |
getLinksCount | GET /links/count | count | getLinksCountQuerySchema | z.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 ID | Zod Schema Binding | Target Metric Dimension |
|---|---|---|
AnalyticsCount | analyticsResponse.count | Aggregate count metrics |
AnalyticsTimeseries | analyticsResponse.timeseries | Timeseries datapoints |
AnalyticsContinents | analyticsResponse.continents | Continents breakdown |
AnalyticsCountries | analyticsResponse.countries | Countries breakdown |
AnalyticsRegions | analyticsResponse.regions | Regions breakdown |
AnalyticsCities | analyticsResponse.cities | Cities breakdown |
AnalyticsDevices | analyticsResponse.devices | Device types |
AnalyticsBrowsers | analyticsResponse.browsers | Browsers breakdown |
AnalyticsOS | analyticsResponse.os | Operating systems |
AnalyticsTriggers | analyticsResponse.triggers | Click triggers |
AnalyticsEventNames | analyticsResponse.event_names | Event names |
AnalyticsReferers | analyticsResponse.referers | Referers breakdown |
AnalyticsRefererUrls | analyticsResponse.referer_urls | Referer URLs |
AnalyticsTopLinks | analyticsResponse.top_links | Top performing links |
AnalyticsTopUrls | analyticsResponse.top_urls | Top 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 Constant | Response Type | Discriminator Value |
|---|---|---|
clickEventResponseSchema | Click telemetry event | click |
leadEventResponseSchema | Lead conversion event | lead |
saleEventResponseSchema | Sale conversion event | sale |
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 ID | HTTP Method & Path | Query Schema Binding | Response Schema | Tag |
|---|---|---|---|---|
listDomains | GET /domains | getDomainsQuerySchema | z.array(DomainSchema) | Domains |
getTags | GET /tags | getTagsQuerySchema | z.array(LinkTagSchema) | Tags |
listFolders | GET /folders | listFoldersQuerySchema | z.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 ID | HTTP Method & Path | Primary Schema | Tag |
|---|---|---|---|
getCustomers | GET /customers | CustomerEnrichedSchema | Customers |
getCustomer | GET /customers/{id} | CustomerEnrichedSchema | Customers |
listDiscountCodes | GET /discount-codes | DiscountCodeSchema | Discount Codes |
retrievePartnerLinks | GET /partners/links | ProgramPartnerLinkSchema | Partners |
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
Related
On This Page
OverviewOpenAPI Specification GenerationDocument Composition and Zod IntegrationRoute Handler and CachingWorkspace Authentication and AuthorizationRequest Authentication and Token ResolutionWorkspace Resolution and MembershipRate Limiting and TelemetryClient Publishable Key AuthenticationPublishable Key Validation and Request FlowError Handling and Telemetry CaptureLink Operations and SchemasRoute Definitions and Query OperationsAnalytics and Event TelemetryAnalytics Endpoint and Response SchemaRaw Event Streaming and SchemasDomain and Organization Resource ManagementWorkspace Domain and Entity Query OperationsTags and Folders Management SchemasPartner and Commerce Resource EndpointsCustomer Entity EndpointsDiscount Code Schemas and PathsPartner Program Telemetry and AnalyticsRelated