---
title: "Payout Processing"
description: "Payout processing orchestrates the end-to-end lifecycle of partner commissions, converting accumulated earnings into distributed funds through integrated financial providers like Stripe, PayPal, an..."
last_updated: "2026-10-05T05:07:35.16912+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/payout-processing"
---

<details>
<summary>Relevant source files</summary>

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

- [apps/web/lib/partners/create-stablecoin-payout.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/create-stablecoin-payout.ts)
- [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts)
- [apps/web/app/ee/api/cron/payouts/balance-available/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts)
- [apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts)
- [apps/web/app/ee/api/cron/payouts/process/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/process/route.ts)
- [apps/web/lib/partners/create-stripe-transfer.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/create-stripe-transfer.ts)
- [apps/web/app/ee/api/cron/payouts/send-stripe-payout/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/send-stripe-payout/route.ts)
- [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts)
- [apps/web/app/ee/api/cron/payouts/payout-paid/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/payout-paid/route.ts)
- [apps/web/lib/tremendous/send-tremendous-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tremendous/send-tremendous-payouts.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/payouts/payoutId/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/payouts/%5BpayoutId%5D/page.tsx)
- [apps/web/lib/actions/partners/confirm-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts)
- [apps/web/app/ee/api/cron/trigger-withdrawal/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/trigger-withdrawal/route.ts)
- [apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/process/route.ts)
- [apps/web/app/ee/partners.dub.co/invoices/payoutId/route.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/invoices/%5BpayoutId%5D/route.tsx)
- [apps/web/lib/payouts/recompute-partner-payout-state.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/payouts/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/admin.dub.co/(dashboard)/payouts/page.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/payouts/payout-table.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/payouts/payout-table.tsx)
- [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-external-payouts.ts)
- [apps/web/lib/constants/payouts-supported-countries.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/constants/payouts-supported-countries.ts)
- [apps/web/ui/partners/confirm-payouts-sheet.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/confirm-payouts-sheet.tsx)
- [apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts)
- [packages/email/src/templates/partner-payout-processed.tsx](https://github.com/blade47/dub/blob/HEAD/packages/email/src/templates/partner-payout-processed.tsx)
- [apps/web/scripts/customers/framer/split-bounty-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/framer/split-bounty-payouts.ts)
- [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts)
- [apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts)
- [apps/web/ui/partners/payout-status-descriptions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/payout-status-descriptions.ts)
- [apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts)
- [packages/email/src/templates/partner-payout-confirmed.tsx](https://github.com/blade47/dub/blob/HEAD/packages/email/src/templates/partner-payout-confirmed.tsx)
- [apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts)
</details>

## Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/create-stablecoin-payout.ts#L38-L46), [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:59-92](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L59-L92), [apps/web/app/ee/api/cron/payouts/balance-available/route.ts:26-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/balance-available/route.ts#L26-L39), [apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts:23-63](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts#L23-L63), [apps/web/lib/actions/partners/confirm-payouts.ts:52-78](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L52-L78), [apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts:219-264](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts#L219-L264)

## Commission Aggregation and Eligibility

### Commission Aggregation and Eligibility

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_`.

Sources: [apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts:219-264](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts#L219-L264)

### Aggregation Call-Chain Execution Walkthrough

The aggregation process follows a strict sequence to prevent race conditions across concurrent workers:
1. `aggregateDueCommissionsForPartner()` sorts input commissions by `createdAt` and instantiates or fetches a target payout record.
2. Raw SQL execution via `prisma.$executeRaw` updates candidate commissions to `processed` status and links them to the payout while verifying that target payout statuses remain mutable.
3. `prisma.commission.aggregate()` calculates the total earnings sum for the active payout to prevent stale precomputed balances.
4. Raw SQL updates the Payout table with the aggregated amount and updates `periodEnd` if a new payout was created.
5. `prisma.commission.findMany()` verifies successfully claimed commissions, which are subsequently passed to `trackCommissionStatusUpdate()` for activity logging.

Sources: [apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts:234-362](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts#L234-L362)

> [!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.

Sources: [apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts:266-284](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/process/route.ts#L266-L284)

### Eligibility Filtering and Cutoff Boundaries

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`.

Sources: [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:59-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L59-L82)

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.

Sources: [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:107-119](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L107-L119)

## Fee Calculation and FX Quoting

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L138-L208), [apps/web/lib/payouts/recompute-partner-payout-state.ts:19-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts#L19-L136)

### Fee Calculation and Multi-Currency FX Quoting

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L25-L28), [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:138-164](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L138-L164)

```typescript
const nonUsdPaymentMethodTypes = {
  sepa_debit: "eur",
  acss_debit: "cad",
} as const;
```

Sources: [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:25-28](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L25-L28)

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.

Sources: [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:192-208](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L192-L208)

> [!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.

Sources: [apps/web/app/ee/api/cron/payouts/process/process-payouts.ts:203-208](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/process/process-payouts.ts#L203-L208)

### Partner Payout State Recomputation

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.

Sources: [apps/web/lib/payouts/recompute-partner-payout-state.ts:19-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts#L19-L48)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts#L7-L12), [apps/web/lib/payouts/recompute-partner-payout-state.ts:68-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts#L68-L89)

| Payout Method Constant | Enum Reference | Activation Criteria |
| :--- | :--- | :--- |
| Stablecoin | `PartnerPayoutMethod.stablecoin` | Crypto wallet capabilities active (`stablecoinAccount`), wallet address present, and network defined |
| Stripe Connect | `PartnerPayoutMethod.connect` | Connect account payouts enabled (`payouts_enabled === true`) with active transfer capabilities |
| PayPal | `PartnerPayoutMethod.paypal` | Partner email record populated (`partner.paypalEmail`) |
| Tremendous | `PartnerPayoutMethod.tremendous` | Partner email record populated (`partner.tremendousEmail`) |

Sources: [apps/web/lib/payouts/recompute-partner-payout-state.ts:7-12](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts#L7-L12), [apps/web/lib/payouts/recompute-partner-payout-state.ts:55-77](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/payouts/recompute-partner-payout-state.ts#L55-L77)

Supported payout countries and their corresponding available methods are compiled across stablecoin, Connect, and PayPal networks using `PAYOUT_SUPPORTED_COUNTRIES`.

Sources: [apps/web/lib/constants/payouts-supported-countries.ts:10-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/constants/payouts-supported-countries.ts#L10-L22)

## Program Payout Confirmation Workflow

### Program Payout Confirmation Workflow

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.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:52-218](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L52-L218)

### Confirmation Call-Chain Execution

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()`

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:54-222](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L54-L222)

### Input Validation and Eligibility Rules

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.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:30-50](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L30-L50)

> [!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.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:42-50](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L42-L50)

Prior to invoice creation, the server action performs several strict guards:
- Requires the workspace to have a valid `stripeId`.
- Rejects fast settlement requests if `workspace.fastDirectDebitPayouts` is disabled.
- Enforces workspace payout usage limits (`workspace.payoutsUsage + amount > workspace.payoutsLimit`).
- Rejects payout totals falling below `INVOICE_MIN_PAYOUT_AMOUNT_CENTS` ($10).
- Restricts cutoff periods if eligible payouts exceed `CUTOFF_PERIOD_MAX_PAYOUTS`.
- Requires an active webhook subscribed to the `payout.confirmed` event if the invoice includes external payouts in non-internal payout modes.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:79-153](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L79-L153)

### Stripe Payment Method and Mandate Validation

The system retrieves the Stripe payment method using `stripe.paymentMethods.retrieve(paymentMethodId)` and validates that its customer ID matches `workspace.stripeId`.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:156-160](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L156-L160)

| Validation Check | Condition | Error / Action Taken |
| :--- | :--- | :--- |
| Payment Type Support | `!PAYMENT_METHOD_TYPES.includes(paymentMethod.type)` | Throws error restricting supported types |
| Fast Settlement ACH | `fastSettlement && paymentMethod.type !== "us_bank_account"` | Throws error restricting fast settlement to ACH |
| Direct Debit Mandate | `DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(paymentMethod.type)` | Calls `checkPaymentMethodMandate()`; detaches payment method and throws if invalid |

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:162-187](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L162-L187)

> [!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.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:175-187](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L175-L187)

### Invoice Generation and PDF Rendering

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`.

Sources: [apps/web/lib/actions/partners/confirm-payouts.ts:189-218](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/confirm-payouts.ts#L189-L218)

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.

Sources: [apps/web/app/ee/partners.dub.co/invoices/payoutId/route.tsx:36-74](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/invoices/%5BpayoutId%5D/route.tsx#L36-L74)

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.

Sources: [apps/web/app/ee/partners.dub.co/invoices/payoutId/route.tsx:154-236](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/invoices/%5BpayoutId%5D/route.tsx#L154-L236)

## Stripe Connect and Stablecoin Disbursement

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/send-stripe-payout/route.ts#L1-L73), [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:1-155](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts#L1-L155), [apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:1-67](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/trigger-withdrawal/route.ts#L1-L67)

### Queueing and Execution Call Chain

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts#L16-L155), [apps/web/app/ee/api/cron/payouts/send-stripe-payout/route.ts:18-73](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/send-stripe-payout/route.ts#L18-L73), [apps/web/lib/partners/create-stablecoin-payout.ts:38-42](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/create-stablecoin-payout.ts#L38-L42), [apps/web/lib/partners/create-stripe-transfer.ts:26-36](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/create-stripe-transfer.ts#L26-L36)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts#L80-L96), [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:142-146](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts#L142-L146)

### Stripe Financial Account Funding and Withdrawals

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()`.

Sources: [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts:35-78](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts#L35-L78)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/trigger-withdrawal/route.ts#L9-L12), [apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:41-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/trigger-withdrawal/route.ts#L41-L43)

| Balance Component | Calculation Source / Logic | Purpose |
| :--- | :--- | :--- |
| `currentAvailableBalance` | `stripeBalanceData.available.find(b => b.currency === "usd")?.amount ?? 0` | Funds immediately available for payout or withdrawal |
| `currentPendingBalance` | `stripeBalanceData.pending.find(b => b.currency === "usd")?.amount ?? 0` | Funds waiting to settle in Stripe |
| `currentNetBalance` | `currentPendingBalance < 0 ? currentAvailableBalance + currentPendingBalance : currentAvailableBalance` | Accounts for negative pending adjustments |
| `reservedBalance` | Hardcoded constant `30_000_00` ($30,000) | Ensures minimum operating liquidity remains in the account |
| `balanceToWithdraw` | `currentNetBalance - payoutsToBeSent - reservedBalance` | Net amount submitted to `stripe.payouts.create()` |

Sources: [apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:28-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/trigger-withdrawal/route.ts#L28-L43), [apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:59-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/trigger-withdrawal/route.ts#L59-L62)

> [!NOTE]
> If `balanceToWithdraw` calculates to less than or equal to zero, the withdrawal cron job safely exits without invoking `stripe.payouts.create()`.

Sources: [apps/web/app/ee/api/cron/trigger-withdrawal/route.ts:53-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/trigger-withdrawal/route.ts#L53-L57)

## PayPal, Tremendous, and External Channels

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tremendous/send-tremendous-payouts.ts#L23-L31), [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts:9-14](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts#L9-L14), [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:10-14](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts#L10-L14)

### PayPal Batch Payout Execution

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.

Sources: [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:10-51](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts#L10-L51)

> [!NOTE]
> If no eligible PayPal payouts exist for the invoice, execution exits immediately without calling `createPayPalBatchPayout`.

Sources: [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:53-56](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts#L53-L56)

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`.

Sources: [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:58-104](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts#L58-L104)

### Tremendous Campaign Fulfillment

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.

Sources: [apps/web/lib/tremendous/send-tremendous-payouts.ts:23-111](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tremendous/send-tremendous-payouts.ts#L23-L111)

> [!WARNING]
> Total transferable amounts must fall strictly between `TREMENDOUS_MIN_PAYOUT_AMOUNT_CENTS` and `TREMENDOUS_MAX_PAYOUT_AMOUNT_CENTS`; violations throw explicit validation errors.

Sources: [apps/web/lib/tremendous/send-tremendous-payouts.ts:128-138](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tremendous/send-tremendous-payouts.ts#L128-L138)

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.

Sources: [apps/web/lib/tremendous/send-tremendous-payouts.ts:142-245](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tremendous/send-tremendous-payouts.ts#L142-L245)

### External Payouts and Webhook Routing

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/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts:9-120](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts#L9-L120)

| Handler Function | Target Channel | Key Filtering Criteria | Success State Transition |
| :--- | :--- | :--- | :--- |
| `sendPaypalPayouts` | PayPal API | `mode: "internal"`, `method: "paypal"`, `paypalEmail` present | Status updated to `"sent"` |
| `sendTremendousPayouts` | Tremendous API | `mode: "internal"`, `method: "tremendous"`, `tremendousEmail` present | Status updated to `"completed"` |
| `queueExternalPayouts` | Webhook / Email | `mode: "external"`, workspace `payout.confirmed` trigger | Dispatched via `sendWorkspaceWebhook` |

Sources: [apps/web/lib/tremendous/send-tremendous-payouts.ts:71-82](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tremendous/send-tremendous-payouts.ts#L71-L82), [apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts:50-85](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/queue-external-payouts.ts#L50-L85), [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:22-36](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts#L22-L36), [apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:66-74](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts#L66-L74)

## Settlement Lifecycle and Webhook Handlers

### Overview

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.

Sources: [apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts:68-117](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts#L68-L117)

### Asynchronous Settlement & Charge Processing

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts#L23-L96), [apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts:20-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts#L20-L62)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/route.ts#L67-L96), [apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts:64-67](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/charge-succeeded/utils.ts#L64-L67)

### Stripe Connect Webhook Handlers

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts#L5-30), [apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts:5-36](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts#L5-36), [apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts:5-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts#L5-34)

| Webhook Event / Handler | QStash Queue Name | Target Cron Route | Action Performed |
| :--- | :--- | :--- | :--- |
| `balanceAvailable` / `AccountExternalAccountUpdated` | `handle-balance-available` | `/api/cron/payouts/balance-available` | Retrieves Stripe balance, checks dust/pending amounts, creates Stripe payout, and updates matching payouts. |
| `payoutPaid` | `handle-payout-paid` | `/api/cron/payouts/payout-paid` | Updates payout status to `"completed"` with trace ID and notifies partner. |
| `payoutFailed` | `handle-payout-failed` | `/api/cron/payouts/payout-failed` | Receives failure metadata and updates disconnected or errored bank accounts. |

Sources: [apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts:5-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/balance-available.ts#L5-30), [apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts:5-36](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/payout-paid.ts#L5-36), [apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts:5-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/stripe/connect/webhook/payout-failed.ts#L5-34), [apps/web/app/ee/api/cron/payouts/balance-available/route.ts:26-145](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/balance-available/route.ts#L26-145), [apps/web/app/ee/api/cron/payouts/payout-paid/route.ts:21-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/payout-paid/route.ts#L21-50)

### Partner Email Notifications

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/balance-available/route.ts#L7-124), [apps/web/app/ee/api/cron/payouts/payout-paid/route.ts:3-66](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/payout-paid/route.ts#L3-66), [packages/email/src/templates/partner-payout-processed.tsx:23-220](https://github.com/blade47/dub/blob/HEAD/packages/email/src/templates/partner-payout-processed.tsx#L23-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.

Sources: [apps/web/app/ee/api/cron/payouts/balance-available/route.ts:63-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/ee/api/cron/payouts/balance-available/route.ts#L63-89)

## Related

- [Commission Rules and Rewards](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/commission-rules-and-rewards)
- [Stripe Billing and Webhooks](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/external-integrations/stripe-billing-and-webhooks)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
