Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
Payout processing orchestrates the end-to-end lifecycle of partner commissions, converting accumulated earnings into distributed funds through integrated financial providers like Stripe, PayPal, and Tremendous. The system handles scheduled commission aggregation, dynamic fee calculations, workspace payout confirmations, multi-currency conversions, and asynchronous balance settlements while maintaining idempotency and robust audit trails. By automating these workflows through cron jobs and webhooks, the platform ensures accurate disbursement tracking, automated partner notifications, and seamless compliance across internal and external payout channels.
Sources: apps/web/lib/partners/create-stablecoin-payout.ts:38-46, apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:59-92, apps/web/app/ee/api/cron/payouts/balance-available/route.ts:26-39, apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts:23-63, apps/web/lib/actions/partners/confirm-payouts.ts:52-78, apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts:219-264
Partner commissions are aggregated into structured payouts through scheduled cron executions via aggregateDueCommissionsForPartner. This routine processes pending commissions by sorting them chronologically to determine period boundaries (periodStart and periodEnd), and either reuses an existing pending payout or instantiates a new record prefixed with po_.
The aggregation process follows a strict sequence to prevent race conditions across concurrent workers:
aggregateDueCommissionsForPartner() sorts input commissions by createdAt and instantiates or fetches a target payout record.prisma.$executeRaw updates candidate commissions to processed status and links them to the payout while verifying that target payout statuses remain mutable.prisma.commission.aggregate() calculates the total earnings sum for the active payout to prevent stale precomputed balances.periodEnd if a new payout was created.prisma.commission.findMany() verifies successfully claimed commissions, which are subsequently passed to trackCommissionStatusUpdate() for activity logging.Warning
Prisma's updateMany can drop WHERE clauses on MySQL under concurrent workloads, allowing multiple workers to claim the same commissions. The system mitigates this by executing raw SQL statements ($executeRaw) with strict inner joins against mutable payout statuses.
Once commissions are aggregated, payout execution filters eligible records using workspace parameters, selection criteria, and cutoff periods. The processPayouts function executes a conditional updateMany query on payouts using payoutIdSelectionWhere and getPayoutEligibilityFilter, optionally constraining the upper bound of the period via cutoffPeriodValue.
If a program operates under hybrid mode, secondary updates mark payouts linked to partners with payoutsEnabledAt = null as external to route them outside automated internal rails.
Payout fee calculation, multi-currency foreign exchange quoting, and recipient validation are handled during the payout processing pipeline. The system evaluates payment method types, applies fee waivers, generates Stripe FX quotes for non-USD transactions, and recomputes partner eligibility and default payout methods based on active account capabilities.
Sources: apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:138-208, apps/web/lib/payouts/recompute-partner-payout-state.ts:19-136
The system determines the payout fee using the selected Stripe payment method and workspace fee configurations via calculatePayoutFeeForMethod, and then applies any available fee waivers via calculatePayoutFeeWithWaiver. Non-USD payment methods are mapped using the nonUsdPaymentMethodTypes dictionary, which handles specific currency conversions.
Sources: apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:25-28, apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:138-164
const nonUsdPaymentMethodTypes = {
sepa_debit: "eur",
acss_debit: "cad",
} as const;When processing payment methods associated with non-USD currencies (such as sepa_debit for EUR or acss_debit for CAD), the system requests an FX quote via createFxQuote to retrieve the active exchange rate from Stripe before charging the invoice total.
Warning
If Stripe's FX exchange rate returns null, zero, or a negative value, the payout process throws an execution error to prevent quoting or charging incorrect currency amounts.
Partner recipient validation and payout method eligibility are managed by recomputePartnerPayoutState. This function evaluates connected accounts and stablecoin accounts in parallel, checking account statuses, capabilities, and active configurations.
Payout methods are evaluated against a strict priority order defined in PAYOUT_METHOD_PRIORITY. The system filters active methods and preserves the partner's existing default payout method if it remains valid, otherwise falling back to the highest priority available method.
Sources: apps/web/lib/payouts/recompute-partner-payout-state.ts:7-12, apps/web/lib/payouts/recompute-partner-payout-state.ts:68-89
Sources: apps/web/lib/payouts/recompute-partner-payout-state.ts:7-12, apps/web/lib/payouts/recompute-partner-payout-state.ts:55-77
Supported payout countries and their corresponding available methods are compiled across stablecoin, Connect, and PayPal networks using PAYOUT_SUPPORTED_COUNTRIES.
Workspace users initiate payout batch confirmation through the confirmPayoutsAction server action. This workflow validates user permissions, enforces usage limits and minimum invoice amounts, checks payment method mandates for direct debit accounts, and creates the corresponding invoice record in the database.
The payout confirmation workflow executes a specific sequence of validations and state checks before persisting the invoice. The invocation path follows:
confirmPayoutsAction() → getDefaultProgramIdOrThrow() → getProgramOrThrow() → throwIfNoPermission() → prisma.payout.aggregate() → getEligiblePayouts() → stripe.paymentMethods.retrieve() → checkPaymentMethodMandate() → prisma.$transaction() → createTremendousCampaignJob.dispatch()
The incoming request is parsed and validated using a Zod schema defined in confirmPayoutsSchema. It validates workspace parameters, selected or excluded payout identifiers, fast settlement flags, and financial totals.
Warning
Requests cannot combine selectedPayoutIds with excludedPayoutIds within the same operation. The schema validation super-refine rule adds a custom issue if both parameters contain values.
Prior to invoice creation, the server action performs several strict guards:
stripeId.workspace.fastDirectDebitPayouts is disabled.workspace.payoutsUsage + amount > workspace.payoutsLimit).INVOICE_MIN_PAYOUT_AMOUNT_CENTS ($10).CUTOFF_PERIOD_MAX_PAYOUTS.payout.confirmed event if the invoice includes external payouts in non-internal payout modes.The system retrieves the Stripe payment method using stripe.paymentMethods.retrieve(paymentMethodId) and validates that its customer ID matches workspace.stripeId.
Caution
If a direct debit payment method lacks an active mandate during verification, the system automatically detaches the payment method via stripe.paymentMethods.detach(paymentMethodId) before throwing an error.
Once all validations pass, a Prisma transaction (tx.invoice.create) generates an invoice record. It calculates the next sequential invoice number by counting existing workspace invoices, padding the number to four digits, and combining it with workspace.invoicePrefix.
Partners can download or view generated PDF invoices via the route handler at GET /partners.dub.co/invoices/[payoutId]. This route fetches the payout and program details using prisma.payout.findUniqueOrThrow, verifies partner authorization, and checks that the payout status is included in INVOICE_AVAILABLE_PAYOUT_STATUSES and that its mode is not external.
The PDF layout is compiled using @react-pdf/renderer and react-pdf-tailwind, including Dub's corporate address, US EIN tax ID, invoice number matching the payout identifier, payee details, and conditional tax notices such as EU or Australian GST/VAT reverse charge disclosures.
Disbursement processing handles the final transfer of funds to partners via Stripe Connect or stablecoins, as well as automated financial account funding and liquidity management cron routines.
Sources: apps/web/app/ee/api/cron/payouts/send-stripe-payout/route.ts:1-73, apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:1-155, apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:1-67
When an invoice charge succeeds, payouts are queued and processed asynchronously through specific route handlers and helper libraries. The execution flow follows this exact call chain:
queueStripePayouts() → QStash queue (send-stripe-payout) → POST /api/cron/payouts/send-stripe-payout → createStripeTransfer() or createStablecoinPayout()
Sources: apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:16-155, apps/web/app/ee/api/cron/payouts/send-stripe-payout/route.ts:18-73, apps/web/lib/partners/create-stablecoin-payout.ts:38-42, apps/web/lib/partners/create-stripe-transfer.ts:26-36
Important
The chargeId is passed as a source_transaction for card payouts to account for settlement latency, but is omitted for ACH and SEPA transfers since those payment methods settle asynchronously via charge.succeeded webhooks after approximately four days.
Sources: apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:80-96, apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:142-146
Stablecoin payouts require pre-funding Dub's Stripe financial account. When fundsAvailable is true, queueStripePayouts aggregates stablecoin payouts meeting or exceeding MIN_WITHDRAWAL_AMOUNT_CENTS, adds STABLECOIN_PAYOUT_FIXED_FEE_CENTS per payout, and calls fundFinancialAccount().
Separately, the withdrawal cron job (GET /api/cron/trigger-withdrawal) runs twice daily at 1 AM and 1 PM UTC to withdraw excess funds from Stripe back to the bank account while keeping a reserved operational balance.
Sources: apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:9-12, apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:41-43
Sources: apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:28-43, apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:59-62
Note
If balanceToWithdraw calculates to less than or equal to zero, the withdrawal cron job safely exits without invoking stripe.payouts.create().
Disbursements routed outside of Stripe operate through dedicated processor integrations and asynchronous channel handlers. PayPal batch payouts, Tremendous reward campaigns, and external webhook deliveries manage specialized partner payout flows when internal invoicing or external settlement modes are selected.
Sources: apps/web/lib/tremendous/send-tremendous-payouts.ts:23-31, apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts:9-14, apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:10-14
The sendPaypalPayouts function validates that the invoice payout mode is not set to external, queries for internal processing payouts utilizing the PayPal payment method where partners have active PayPal email addresses and enabled payouts, and submits them to the PayPal batch payout API.
Note
If no eligible PayPal payouts exist for the invoice, execution exits immediately without calling createPayPalBatchPayout.
Once the batch payout is created, matching payout records transition to a "sent" status with a recorded paidAt timestamp, triggering background notification emails and referral commission queue jobs via Vercel's waitUntil.
The sendTremendousPayouts function handles gift card and digital reward distribution via the Tremendous API. It fetches partner data, verifies active payout settings and Tremendous email credentials, and aggregates previously processed payouts alongside current invoice payouts.
Warning
Total transferable amounts must fall strictly between TREMENDOUS_MIN_PAYOUT_AMOUNT_CENTS and TREMENDOUS_MAX_PAYOUT_AMOUNT_CENTS; violations throw explicit validation errors.
Orders are dispatched using an idempotency key generated from partner and payout identifiers. If the order status is successfully marked as "EXECUTED" with a valid delivery link, underlying payouts are updated to "completed", and associated commissions are processed in batches of 250.
When invoices specify external payout processing (payoutMode === "external"), queueExternalPayouts bypasses internal money movement, searches for workspace webhooks configured with the payout.confirmed trigger, validates payload schemas, and dispatches webhook notifications alongside batch confirmation emails.
Sources: apps/web/lib/tremendous/send-tremendous-payouts.ts:71-82, apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts:50-85, apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:22-36, apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:66-74
The settlement lifecycle and webhook handlers manage asynchronous balance checking, Stripe Connect webhook ingestion, and partner email notifications. When charges succeed, settlement timing is validated against underlying balance transactions before payouts are queued or scheduled via QStash.
The charge-succeeded cron route parses incoming invoice payloads, updates payout methods from partner defaults, and evaluates whether funds have settled for card payments. If funds are not immediately available, delayed payouts are scheduled using QStash with a 10-minute deduplication window and parallelism settings.
Sources: apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts:23-96, apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts:20-62
Warning
Payout methods such as stablecoins, PayPal, and Tremendous require funds to be fully settled before queuing; otherwise, funds are not released to prevent fronting card charges subject to reversal.
Sources: apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts:67-96, apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts:64-67
Stripe Connect webhooks capture balance updates, payout completions, and payout failures, pushing payloads into dedicated QStash queues for asynchronous processing.
Sources: apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts:5-30, apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts:5-36, apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts:5-34
Sources: apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts:5-30, apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts:5-36, apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts:5-34, apps/web/app/ee/api/cron/payouts/balance-available/route.ts:26-145, apps/web/app/ee/api/cron/payouts/payout-paid/route.ts:21-50
Email templates built with React Email notify partners across key lifecycle events, including withdrawal initiation, successful transfer completion, and action required for invalid bank accounts.
Sources: apps/web/app/ee/api/cron/payouts/balance-available/route.ts:7-124, apps/web/app/ee/api/cron/payouts/payout-paid/route.ts:3-66, packages/email/src/templates/partner-payout-processed.tsx:23-220
Tip
Currencies such as HUF and TWD are validated to ensure balances are evenly divisible by 100, skipping dust amounts under 1 unit while requeuing checks if a positive pending balance exists.