Commissions and Payouts
Purpose and Scope
Commissions and payouts are the financial layer of Dub's affiliate-program system. A commission represents money earned by a partner from a program activity, such as a sale, lead, referral, or manual adjustment. A payout represents the later settlement step where eligible commissions are grouped for payment to the partner. This page explains the source-backed lifecycle that connects commission status, payout aggregation, public API surfaces, and the internal admin views used to monitor the flow. It is intended for developers integrating with Dub's partner-program APIs or contributors changing how affiliate earnings move from pending activity to payable financial records.
Sources: apps/web/lib/openapi/commissions/index.ts, apps/web/lib/openapi/payouts/index.ts
Dub's product documentation describes a partner-facing lifecycle where commissions usually begin as Pending, become Processed after they are added to a payout, and then become Paid when the payout is paid by the program. It also describes exceptional commission states such as refunded, fraud, canceled, and duplicate. The repository evidence aligns with that model by treating commission status as a typed filter, by exposing commission mutation routes, and by running a cron process that searches for pending commissions whose holding period has elapsed. The important implementation concept is that commission state is not only display metadata; it drives which rows are eligible for payout aggregation and which views can summarize earnings.
Sources: apps/web/lib/commissions/schema.ts, apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts
Relevant Source Files
apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts— Defines the scheduled aggregation job for due commissions, including signature verification, optional program filtering, holding-period grouping, batching, and pending-commission selection.apps/web/lib/commissions/schema.ts— Defines commission analytics query filters and response shapes used by dashboard analytics, including grouping by time, partner, group, partner tag, and type.apps/web/lib/openapi/commissions/index.ts— Registers the public OpenAPI path family for creating, listing, updating, and bulk-updating commissions.apps/web/lib/openapi/payouts/index.ts— Registers the public OpenAPI path family for listing payouts.apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx— Implements the enterprise admin commissions dashboard with program filtering, time-series data, totals, and tabular program reporting.apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/page.tsx— Implements the enterprise admin payouts dashboard with payout, fee, and total tabs, invoice data, status/program filtering, comparison-period logic, and status badges.
Core Financial Concepts
A commission should be read as an earning event plus its accounting state. In the schema layer, commission analytics inherit common time filters from the analytics query schema and add affiliate-specific filters: groupId, partnerTagId, partnerId, type, and status. The status field is validated against Prisma's CommissionStatus enum, which keeps dashboard and API consumers aligned with the database-level status vocabulary. The analytics response shapes then separate category rows, time-series rows, and partner rows, giving product surfaces a consistent way to show aggregate earnings, counts, and partner-level detail without requiring every caller to interpret raw commission records.
Sources: apps/web/lib/commissions/schema.ts
A payout should be read as the settlement container for payable commissions. The OpenAPI registry exposes payouts as a smaller public surface than commissions: /payouts supports GET through listPayouts, while commission routes support creation, listing, single-record update, and bulk update. That split is an important boundary. Integrations can create or correct commission records through the commissions API family, but payout records are primarily retrieved from the payout API family and operationally assembled by backend workflow. This separation helps preserve accounting control around settlement while still making earning history and payout status visible to program operators and partners.
Sources: apps/web/lib/openapi/commissions/index.ts, apps/web/lib/openapi/payouts/index.ts
API Components
The commissions OpenAPI index is the concise route map for the public commission resource family. It registers POST /commissions for createCommission, GET /commissions for listCommissions, PATCH /commissions/{id} for updateCommission, and PATCH /commissions/bulk for bulkUpdateCommissions. The names communicate the intended integration model: create individual manual or programmatic commissions, retrieve collections for reporting or reconciliation, adjust one commission when a status or amount changes, and apply bulk changes when operational review needs to update many records at once.
Sources: apps/web/lib/openapi/commissions/index.ts
| Resource family | Method and path | Registered handler | Source-backed role |
|---|---|---|---|
| Commissions | POST /commissions | createCommission | Create a commission record. |
| Commissions | GET /commissions | listCommissions | Retrieve commission records for reporting or reconciliation. |
| Commissions | PATCH /commissions/{id} | updateCommission | Update one commission by identifier. |
| Commissions | PATCH /commissions/bulk | bulkUpdateCommissions | Update multiple commissions in one operation. |
| Payouts | GET /payouts | listPayouts | Retrieve payout records. |
The payouts OpenAPI index intentionally exposes only GET /payouts. That does not make payouts less central; it means the settlement lifecycle is represented as a read-oriented API for external consumers while internal jobs and admin tooling handle aggregation and monitoring. When building an integration, use the commission endpoints for earning-event creation or correction and use the payout endpoint for payment-state visibility. When building internal product features, treat payout creation and commission-to-payout movement as operational flows that must remain consistent with holding periods, program settings, partner grouping, and status transitions.
Sources: apps/web/lib/openapi/payouts/index.ts, apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts
Aggregation Flow
The aggregation route is the strongest source signal for how pending commissions become payout candidates. It is marked force-dynamic, defines a BATCH_SIZE of 1000, and accepts an optional programId in a Zod schema when invoked by POST. The handler verifies Vercel signatures for GET cron invocations and QStash signatures for POST invocations, which gives the same workflow both a scheduled entry point and a queued recursive entry point. The comment states that the job runs once per hour and calls itself recursively to look through all pending commissions available.
Sources: apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts
The job first groups partner groups by holdingPeriodDays, optionally scoped to one program. It then loads the partner groups for each holding period and selects their program identifiers, names, and workspace identifiers. This grouping matters because holding periods are a program-configuration control that delay payout eligibility after a commission is created. Processing by holding-period bucket lets the job apply the correct time cutoff to each group instead of treating all commissions as immediately payable. It also keeps logs meaningful: the route logs how many partner groups were found for each holding-period value and skips buckets when no due commissions remain.
Sources: apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts
Due commission selection is intentionally conservative. The query looks for commissions with status: "pending" whose enrollment group is in the current partner-group bucket. If the holding period is greater than zero, normal commissions must have a createdAt timestamp older than the holding-period cutoff. The route makes an explicit exception for commission types custom and referral, described in comments as covering manual commissions, referral commissions, and clawbacks; those are included even when the holding-period gate would otherwise apply. Results are ordered by createdAt ascending and limited to the batch size, which makes recursive processing deterministic and prevents one cron execution from loading an unbounded amount of work.
Sources: apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts
Admin Monitoring Surfaces
The admin commissions dashboard is a client-side enterprise page that fetches /api/admin/commissions with the current query string and an explicit timezone. It builds a program filter from returned programs, tracks active filters through router query parameters, and resets pagination when filters change. The page also defines a commissions tab and a disabled fees tab, maps time-series rows into chart data, and calculates totals for commissions and fees. In practice, this page is the operator-facing lens over commission volume by program and time range, not the API mutation surface itself.
Sources: apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx
The admin payouts dashboard follows the same pattern but focuses on settlement reporting. It fetches /api/admin/payouts, reads interval, start, end, status, programId, and tab from search parameters, and validates the selected tab against the supported payouts, fees, and total views. It models invoice rows with date, program identity, InvoiceStatus, amount, fee, and total, then derives unique programs from invoice data for filtering. It also computes a previous-period query range from the returned time-series data, enabling comparison views without hard-coding a fixed interval.
Sources: apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/page.tsx
These dashboards are useful implementation guides because they show the reporting dimensions the backend must preserve. Commission analytics need time range, timezone, program, partner, group, tag, type, and status dimensions. Payout analytics need invoice status, program identity, fee accounting, and total settlement amounts. The UI imports payout status badges, currency formatting, table helpers, chart components, and router utilities, so changes to the financial lifecycle should be checked against both API contracts and these operator views. A backend-only change that updates status naming or aggregation timing can silently break filters, badges, totals, or period comparisons.
Sources: apps/web/lib/commissions/schema.ts, apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx, apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/page.tsx
Implementation Notes and Next Steps
When extending commissions, start from the public contract and then follow the lifecycle. Add or adjust API behavior in the commission OpenAPI modules, keep analytics filters compatible with commissionAnalyticsQuerySchema, and verify the admin commissions page can still build its chart and table data. When extending payouts, remember that the public API currently exposes listing while the cron job performs eligibility scanning and aggregation. Any change to holding-period rules, commission types, or statuses should be evaluated against the hourly aggregation route and the payout dashboard's assumptions about invoices, statuses, fees, and totals.
Sources: apps/web/lib/openapi/commissions/index.ts, apps/web/lib/openapi/payouts/index.ts, apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts, apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/page.tsx
For product and integration work, the practical sequence is: create or update commissions through the commissions API, let pending commissions age according to partner-group holding periods, allow the aggregation job to move due commissions into payout processing, and read payout state through the payouts API or admin dashboard. For deeper work, read the API reference pages for commissions and payouts next, then inspect partner-program and bounty docs to understand the upstream events that create financial obligations. If you are changing notifications or partner-facing payout instructions, pair this lifecycle with the email-workflow source files for the exact templates and delivery behavior.