Bulk Links and Counts

Purpose and Scope

Bulk link operations are for clients that need to manage link inventories at workspace scale instead of one record at a time. In Dub’s public API documentation, the bulk family covers creating up to one hundred links, updating up to one hundred links with shared data, and deleting up to one hundred links by identifier. Count retrieval is the companion read operation: it lets a client ask how many links match a set of filters before planning a batch job, after verifying a migration, or while rendering a dashboard segment.

The repository evidence for this page is strongest around the count operation. Dub exposes a Links API operation named getLinksCount, maps it to a workspace-scoped route, validates filters, enforces the links.read permission, and delegates the database aggregation to a shared function. That shared function can return either a scalar count or grouped aggregate rows depending on the requested grouping. Separate source files also show an enterprise admin count route, a legacy route re-export, and a partner-facing OpenAPI operation for retrieving a partner’s links. Sources: apps/web/lib/openapi/links/get-links-count.ts, apps/web/app/api/links/count/route.ts, apps/web/lib/api/links/get-links-count.ts, apps/web/app/(ee)/api/admin/links/count/route.ts, apps/web/app/api/(old)/projects/[slug]/links/count/route.ts, apps/web/lib/openapi/partners/retrieve-partner-links.ts

Use this page when designing workflows that affect or summarize many links. A migration might count the links on a domain, bulk update the matching records with new tracking parameters, and then count again to confirm the resulting segment. A cleanup tool might retrieve a candidate set through normal list or partner-link APIs, show an operator a preflight total, and only then call bulk delete. A reporting surface might use grouped counts to populate filters such as domain, folder, user, or tag without requiring analytics click data.

Relevant Source Files

  • apps/web/lib/openapi/partners/retrieve-partner-links.ts defines the partner-facing OpenAPI operation for retrieving a partner’s links by partner ID or tenant ID, which is useful context for affiliate-oriented link collections.
  • apps/web/app/(ee)/api/admin/links/count/route.ts implements the admin count route, including grouping by domain, tag ID, or user ID and filtering out the legal user ID.
  • apps/web/app/api/(old)/projects/[slug]/links/count/route.ts re-exports the current links count route so older project-style paths share the canonical implementation.
  • apps/web/app/api/links/count/route.ts implements the workspace-scoped GET /api/links/count handler, parses query filters, validates link filters, and requires links.read.
  • apps/web/lib/api/links/get-links-count.ts contains the Prisma count and group-by logic used by the workspace route.
  • apps/web/lib/openapi/links/get-links-count.ts declares the OpenAPI operation metadata, query schema, response schema, tag, and token security for link counts.

Public API Surface

The OpenAPI definition presents count retrieval as a Links operation with operation ID getLinksCount and a Speakeasy name override of count. Its summary is Retrieve links count, and its description says it retrieves the number of links for the authenticated workspace. The request parameters come from getLinksCountQuerySchema, and the documented successful response is a number described as the number of links matching the query. The operation is tagged under Links and uses token security, so API clients should treat it as an authenticated workspace resource rather than a public analytics endpoint. Sources: apps/web/lib/openapi/links/get-links-count.ts

The runtime route is GET /api/links/count. It is wrapped with withWorkspace, which supplies request headers, search parameters, the current workspace, and the authenticated session to the handler. The route parses the query with getLinksCountQuerySchema, passes the accepted filters through validateLinksQueryFilters, and then calls getLinksCount with the workspace identifier and resolved folder identifiers. The response is serialized through NextResponse.json while preserving headers from the workspace wrapper. This route-level split keeps authentication and validation close to the HTTP boundary. Sources: apps/web/app/api/links/count/route.ts

Bulk mutation endpoints are described in the official API docs as POST /links/bulk, PATCH /links/bulk, and DELETE /links/bulk. Bulk create creates up to one hundred links for the authenticated workspace. Bulk update changes up to one hundred links with the same data and is intended for tasks such as tagging many links, setting the same expiration date, or applying common UTM parameters. Bulk delete accepts up to one hundred link IDs and returns a deleted count. The docs also warn that bulk create, update, and delete do not trigger webhook events, and bulk update cannot change a link’s domain or key.

Count Query Model and Filtering

The shared count implementation begins with a strict workspace boundary: every query includes the workspace ID as the project identifier. From there, the function builds a Prisma filter from the accepted query parameters. Search is implemented as a match against either the short link or the destination URL. Archived links are excluded by default unless showArchived is enabled. Domain, user, and tenant filters are applied when supplied, but domain and user filters are intentionally skipped when the request is grouping by that same dimension. Sources: apps/web/lib/api/links/get-links-count.ts

Folder handling is more nuanced because count endpoints often support dashboard navigation and folder sidebars. When validated folderIds are supplied, the query counts links in those folders as well as links with no folder. When explicit validated folder IDs are not supplied and the request is not grouping by folder, the query narrows to the requested folderId or to unfiled links. When grouping by folder, the implementation avoids pre-filtering by a single folder so that the aggregate can actually describe the distribution across folders. Sources: apps/web/lib/api/links/get-links-count.ts, apps/web/app/api/links/count/route.ts

Tag filtering supports several client shapes. The implementation combines the singular tagId and plural tagIds parameters with combineTagIds, then applies a relation filter that requires at least one matching tag identifier. If tag identifiers are absent but tag names are present, the query filters through the nested tag relation by name. A separate withTags flag requires that a link has any tag at all. These options let a client verify the effect of bulk tagging, build tag-filtered inventory views, or distinguish untagged cleanup candidates from tagged campaign links. Sources: apps/web/lib/api/links/get-links-count.ts

Grouped Counts, Admin Counts, and Compatibility

Without grouping, getLinksCount returns a plain Prisma link count for the assembled filter. With grouping, it changes aggregation strategy. Grouping by domain, user ID, or folder ID uses prisma.link.groupBy, groups by the requested field, asks Prisma for _count, and orders results by descending count for that field. Grouping by tag ID uses prisma.linkTag.groupBy instead, because tag membership is represented through a relation rather than a direct field on the link row. Sources: apps/web/lib/api/links/get-links-count.ts

The admin count route has a similar purpose but a broader trust boundary. It is wrapped with withAdmin, accepts search parameters, and supports grouping by domain, tag ID, or user ID. Its base filter excludes LEGAL_USER_ID, supports search across short links and URLs, and avoids applying a domain filter when the user is grouping by domain. When tag IDs are provided, the route accepts comma-separated input and applies it through the link tags relation. Non-tag grouped admin results are capped with take: 500, which is appropriate for an operational overview rather than a full export. Sources: apps/web/app/(ee)/api/admin/links/count/route.ts

The legacy project route is deliberately small: it re-exports the current links count route. That compatibility layer matters for maintainers because it prevents old project-shaped API paths from diverging from the current workspace implementation. If parsing, permission requirements, response headers, or validation behavior changes, the canonical route and shared counting function are the places to update. The old route then receives the same behavior automatically instead of becoming a second implementation with different edge cases. Sources: apps/web/app/api/(old)/projects/[slug]/links/count/route.ts, apps/web/app/api/links/count/route.ts

Bulk Operation Reference

OperationMethod and pathPrimary inputNotable behavior
Bulk create linksPOST /links/bulkArray of link creation objectsUp to 100 links for the authenticated workspace; official docs note webhook events are not sent for bulk creation.
Bulk update linksPATCH /links/bulklinkIds or externalIds, plus shared dataUp to 100 links updated with the same data; official docs say domain and key cannot be updated through this endpoint.
Bulk delete linksDELETE /links/bulkQuery parameter linkIdsUp to 100 IDs; non-existing IDs are ignored; official docs note webhook events are not triggered.
Retrieve links countGET /api/links/count / OpenAPI Links countQuery filters from getLinksCountQuerySchemaRequires workspace authentication and links.read; documented public response is a matching count.

A safe bulk workflow usually starts with counting, not mutation. For example, a client can count all non-archived links on a domain, present the number to an operator, and then issue a bulk update for a selected set of identifiers. After the update, the client can count with tag names, the withTags flag, an expiration-related list query, or the same domain filter to confirm that the intended segment changed. This pattern is especially helpful because bulk endpoints intentionally trade per-link event behavior for throughput.

Deletion deserves a stricter flow. The official docs identify bulk delete as destructive and irreversible, and they also state that non-existing IDs are ignored. A responsible client should therefore treat a count as a preflight aid, not as proof that every ID will be deleted at the moment the request executes. Between the preflight and the deletion, another actor may update, archive, or remove links. Use count results to make the user aware of scale, then rely on the delete response’s deleted count to report what actually happened.

Partner workflows are adjacent to bulk and count behavior because affiliate programs often manage link collections tied to partners or tenants. The partner OpenAPI operation retrievePartnerLinks retrieves a partner’s links by partner ID or tenant ID, returns an array of ProgramPartnerLinkSchema, belongs to the Partners tag, and uses token security. It does not implement bulk mutation, but it shows that partner-associated links are first-class API data. An integration can retrieve partner links, decide which global link operations are safe, and use count filters such as tenant or tag to summarize the collection. Sources: apps/web/lib/openapi/partners/retrieve-partner-links.ts

For API consumers, the main design question is whether you need inventory counts, link mutations, or analytics. Counts answer how many link records match a filter. Bulk endpoints mutate link records in bounded batches. Analytics endpoints answer performance questions such as clicks, devices, locations, referrers, UTM dimensions, and conversions. Keep these categories separate in client design so that an operational dashboard does not accidentally use analytics data to infer inventory, and a migration tool does not treat an inventory count as evidence of click or revenue performance.

For maintainers, keep the boundary between route code and database aggregation clear. The route owns authentication, permissions, schema parsing, folder validation, and response construction. The shared count function owns the Prisma translation for filters and grouping. Admin behavior has its own wrapper and cross-workspace assumptions, while the legacy route should remain a compatibility re-export. Next, read the Links API overview for the single-link resource model, the create/retrieve/list page for collection retrieval, and the update/upsert/delete page before designing a bulk mutation workflow.