---
title: "Stripe Billing and Webhooks"
description: "Dub integrates with Stripe to manage enterprise billing, subscription lifecycles, webhook event routing, and automated monetization workflows. The system processes incoming Stripe webhooks with cry..."
last_updated: "2026-10-05T05:07:35.154101+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/external-integrations/stripe-billing-and-webhooks"
---

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

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

- [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/invoice-paid.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts)
- [apps/web/app/ee/api/stripe/webhook/invoice-payment-failed.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/invoice-payment-failed.tsx)
- [apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts)
- [apps/web/app/ee/api/stripe/connect/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/webhook/route.ts)
- [apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts)
- [apps/web/app/ee/api/singular/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts)
- [apps/web/app/api/callback/plain/workspace/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/workspace/route.ts)
- [apps/web/app/ee/api/stripe/connect/v2/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/v2/webhook/route.ts)
- [apps/web/app/api/webhooks/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/route.ts)
- [apps/web/lib/swr/use-webhooks.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-webhooks.ts)
- [apps/web/app/api/dub/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/billing/payment-methods/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/billing/payment-methods/route.ts)
- [apps/web/app/ee/api/hubspot/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/hubspot/webhook/route.ts)
- [apps/web/app/api/webhooks/callback/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/callback/route.ts)
- [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts)
- [apps/web/app/api/workspaces/idOrSlug/billing/manage/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/billing/manage/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/billing/invoices/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/billing/invoices/route.ts)
- [apps/web/app/ee/api/stripe/webhook/customer-subscription-updated.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts)
- [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts)
- [apps/web/app/ee/api/intercom/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/webhook/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/billing/retry-payment/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/billing/retry-payment/route.ts)
- [apps/web/app/ee/api/stripe/webhook/charge-dispute-created.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-dispute-created.ts)
- [apps/web/app/api/workspaces/idOrSlug/billing/activate-paid-plan/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/billing/activate-paid-plan/route.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/webhooks/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/webhooks/page-client.tsx)
- [apps/web/app/ee/api/cron/invoices/retry-failed/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/invoices/retry-failed/route.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/promotion-code-updated.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/promotion-code-updated.ts)
- [apps/web/app/api/dub/webhook/lead-created.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/lead-created.ts)
- [apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts)
</details>

## Overview

Dub integrates with Stripe to manage enterprise billing, subscription lifecycles, webhook event routing, and automated monetization workflows. The system processes incoming Stripe webhooks with cryptographic signature verification, routing events to specialized handlers that maintain workspace plan capabilities, enforce quota limits, and manage custom invoicing. Adjacent components handle multi-mode Stripe Connect events, partner payout attribution, domain renewal workflows, payment failure mitigations, and automated fraud detection to secure platform monetization.

Sources: [apps/web/app/ee/api/stripe/webhook/route.ts:33-105](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L33-L105), [apps/web/app/ee/api/stripe/integration/webhook/route.ts:36-228](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L36-L228), [apps/web/app/ee/api/stripe/connect/webhook/route.ts:24-91](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/webhook/route.ts#L24-L91), [apps/web/app/ee/api/stripe/webhook/charge-dispute-created.ts:9-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-dispute-created.ts#L9-L94), [apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts:9-158](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts#L9-L158)

## Stripe Webhook Routing Architecture

### Overview

The public Stripe webhook entry point is located at `apps/web/app/(ee)/api/stripe/webhook/route.ts` and wrapped with Axiom telemetry logging via `withAxiom`. The handler reads the raw request body text, extracts the `Stripe-Signature` header, and verifies the payload against the environment variable `STRIPE_WEBHOOK_SECRET` using `stripe.webhooks.constructEvent()`.

Sources: [apps/web/app/ee/api/stripe/webhook/route.ts:33-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L33-L42)

> [!WARNING]
> Requests lacking a valid `Stripe-Signature` header or missing `STRIPE_WEBHOOK_SECRET` fail immediately with HTTP status 400 via `logAndRespond("Invalid request", { status: 400 })`.
> Sources: [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L39-L41)

### Event Filtering and Dispatch Routing

Once successfully parsed into a `Stripe.Event` object, the incoming event type is evaluated against a predefined `Set` of eleven `relevantEvents`. If an event type is not contained within this set, the router immediately skips execution and returns a 200 OK response.

Sources: [apps/web/app/ee/api/stripe/webhook/route.ts:37-55](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L37-L55)

The table below details all eleven supported events recognized by `relevantEvents` alongside their corresponding event-handling functions:

| Stripe Event Type | Handler Function | Sources |
| :--- | :--- | :--- |
| `charge.succeeded` | `chargeSucceeded(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L19), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L60-L62) |
| `charge.failed` | `chargeFailed(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L20), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L63-L65) |
| `charge.refunded` | `chargeRefunded(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L21), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L66-L68) |
| `charge.dispute.created` | `chargeDisputeCreated(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L22), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L69-L71) |
| `checkout.session.completed` | `checkoutSessionCompleted(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L23), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L72-L74) |
| `customer.subscription.created` | `customerSubscriptionCreated(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L24), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L75-L77) |
| `customer.subscription.updated` | `customerSubscriptionUpdated(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L25), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L78-L80) |
| `customer.subscription.deleted` | `customerSubscriptionDeleted(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L26), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L81-L83) |
| `invoice.payment_failed` | `invoicePaymentFailed(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L27), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L84-L86) |
| `payment_intent.requires_action` | `paymentIntentRequiresAction(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L28), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L87-L89) |
| `transfer.reversed` | `transferReversed(event)` | [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L30), [apps/web/app/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L90-L92) |

Sources: [apps/web/app/ee/api/stripe/webhook/route.ts:18-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L18-L30), [apps/web/app/ee/api/stripe/webhook/route.ts:59-93](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L59-L93)

### Error Handling and Execution Flow

If an exception occurs during the execution of any specialized event handler within the `switch` statement, the catch block intercepts the error, writes a structured error entry with `log({ message: ..., type: "errors" })`, and returns a HTTP 400 response containing the error message.

Sources: [apps/web/app/ee/api/stripe/webhook/route.ts:94-102](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L94-L102)

When processing succeeds without interruption, the route returns the result formatted via `logAndRespond(...)`.

Sources: [apps/web/app/ee/api/stripe/webhook/route.ts:104](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L104)

## Subscription Lifecycle and Plan Downgrades

### Overview

The subscription lifecycle manages the transition of workspaces between plans, tiers, billing intervals, and trial periods via Stripe webhook events. When a workspace completes a subscription checkout, `checkoutSessionCompleted` validates that the session mode is set to `"subscription"` and that the payment status is either `"paid"` or `"no_payment_required"`. It retrieves the subscription object, determines the price identifier and plan mapping using `getPlanAndTierFromPriceId`, extracts workspace limits and billing periods, and updates the workspace project model in Prisma with its stripe customer identifier, billing cycle start day, plan tier, and individual feature limits.

Sources: [apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:19-75](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts#L19-L75), [apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:80-105](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts#L80-L105)

### Subscription Creation and Checkout Flow

The initialization call-chain executes sequentially upon a successful checkout session completion:
`checkoutSessionCompleted()` → `stripe.subscriptions.retrieve()` → `getPlanAndTierFromPriceId()` → `prisma.project.update()` → `completeOnboarding()` → `onboardingStepCache.mset()` / `createProgram()` / domain registration.

During this sequence, if a workspace transitions from trial status or upgrades, token caches are expired and welcome emails or onboarding tasks are triggered.

```typescript
export async function checkoutSessionCompleted(
  event: Stripe.CheckoutSessionCompletedEvent,
) {
  const checkoutSession = event.data.object;
  if (checkoutSession.mode !== "subscription") {
    return `Session mode not handled, skipping...`;
  }
  const subscription = await stripe.subscriptions.retrieve(
    checkoutSession.subscription as string,
  );
  const priceId = subscription.items.data[0].price.id;
  const { plan, planTier } = getPlanAndTierFromPriceId({ priceId });
  return `Checkout completed for workspace, upgraded to ${plan.name}.`;
}
```

Sources: [apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:19-56](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts#L19-L56), [apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:80-105](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts#L80-L105)

> [!NOTE]
> Sessions operating in `setup` mode are explicitly skipped by `checkoutSessionCompleted` before retrieving subscription records or updating workspace states.

Sources: [apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:24-26](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts#L24-L26)

### Subscription Updates and Plan Changes

When subscriptions are modified, `customerSubscriptionUpdated` filters incoming events to accept statuses `"active"`, `"trialing"`, or `"past_due"`. For past-due events, if the trial ended less than 2 hours ago, the workspace reverts to trial limits. Otherwise, updates are delegated to `updateWorkspacePlan`.

| Plan Check / Trigger | Condition Evaluated | Action Taken | Sources |
| :--- | :--- | :--- | :--- |
| **Non-Hardcoded Plan** | `!newPlan` | Updates `billingCycleEndsAt` and `planPeriod` without standard tier limits. | [apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts#L82-L96) |
| **Yearly to Monthly** | `workspace.planPeriod === "yearly" && planPeriod === "monthly"` | Recomputes workspace usage and clears limit warning emails. | [apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts#L108-L113) |
| **Partner Limit Upgrade** | `workspace.partnersLimit < newPlan.limits.partners && NEW_BUSINESS_PRICE_IDS.includes(priceId)` | Triggers workspace plan, tier, and limit updates in Prisma. | [apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts#L120-L126) |

Sources: [apps/web/app/ee/api/stripe/webhook/customer-subscription-updated.ts:8-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts#L8-L89), [apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts:34-171](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts#L34-L171)

> [!WARNING]
> If a workspace changes plan capabilities such that `canCreateWebhooks` becomes false, associated webhooks are immediately disabled in the database and `webhookEnabled` is set to false.

Sources: [apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts:220-226](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts#L220-L226)

### Subscription Cancellations and Feature Pruning

When a subscription is deleted via `customerSubscriptionDeleted`, the system checks for alternative active or trialing subscriptions. If a fallback subscription exists, `updateWorkspacePlan` applies it. If no active subscriptions remain, the workspace is downgraded to the `"free"` plan, and strict quota recomputations and feature pruning occur.

```mermaid
graph TD
    A["customer.subscription.deleted"] --> B["Active fallback subscription?"]
    B -- Yes --> C["Update workspace to fallback plan"]
    B -- No --> D["Downgrade workspace to free plan"]
    D --> E["Reset limits to FREE_PLAN constants"]
    D --> F["Remove root domain links & domain logos"]
    D --> G["Disable webhooks & expire token caches"]
    D --> H["Delete workspace folders & deactivate partner program"]
    H --> I["Would lose advanced features?"]
    I -- Yes --> J["Strip advanced reward modifiers & pause campaigns"]
    J --> K["Send AdvancedPlanDowngradeNotice email"]
```

Sources: [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:23-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L23-L100), [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:105-267](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L105-L267)

| Design Choice | Benefit | Cost | Sources |
| :--- | :--- | :--- | :--- |
| **Fallback Subscription Check** | Prevents accidental immediate downgrades when a customer replaces a subscription. | Requires extra Stripe API list calls for active and trialing statuses. | [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L74-L100) |
| **Immediate Feature Pruning** | Enforces tier boundaries instantly upon cancellation. | Disables integrations (webhooks, domains) immediately upon termination. | [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L133-L211) |
| **Invoice Voiding on Deletion** | Prevents open or uncollectible invoices from remaining payable after cancellation. | Modifies external Stripe invoice states asynchronously. | [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L210-L210), [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L270-L298) |

Sources: [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:74-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L74-L100), [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:133-211](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L133-L211), [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:210-210](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L210-L210), [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:270-298](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L270-L298)

> [!CAUTION]
> Deleting a subscription triggers `voidLatestInvoiceIfPayable`, which checks if the latest invoice status is `"open"` or `"uncollectible"` and forcefully voids it via Stripe to prevent billing errors on cancelled accounts.

Sources: [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:210-210](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L210-L210), [apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:270-298](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts#L270-L298)

## Charge Events and Domain Billing

### Overview

The `charge.succeeded` Stripe webhook event is handled by `chargeSucceeded`, which coordinates charge validation, invoice completion, and domain registration or payout fulfillment. When a charge succeeds, the event payload exposes a `transfer_group` that maps directly to an internal invoice identifier. If no `transfer_group` is present, the webhook checks whether the customer's workspace has an active `paymentFailedAt` timestamp and resets it to `null` before skipping further invoice processing.

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:12-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L12-L39), [apps/web/app/ee/api/stripe/webhook/route.ts:60-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L60-L62)

When an `invoiceId` is found via `transfer_group`, the database is queried for the corresponding invoice. If the invoice is already marked as `"completed"`, processing is short-circuited. Otherwise, the invoice record is updated with the charge's receipt URL, completed status, payment timestamp, and serialized charge metadata. Depending on the invoice type, the system branches into partner payout processing or domain renewal workflows.

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:41-74](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L41-L74)

### Call-Chain Execution Walkthroughs

#### Partner Payout Execution Flow

1. `POST` receives the raw webhook request body and Stripe signature header, constructs the event via `stripe.webhooks.constructEvent`, and routes the `"charge.succeeded"` type to `chargeSucceeded`.
2. `chargeSucceeded` reads `charge.transfer_group` as `invoiceId`, retrieves and updates the `Invoice` record to status `"completed"`, and inspects `invoice.type`. Because the type equals `"partnerPayout"`, it hands control to `processPayoutInvoice`.
3. `processPayoutInvoice` counts incomplete payouts linked to the invoice ID and, if any exist, publishes a JSON payload to QStash via `qstash.publishJSON` targeting the endpoint `/api/cron/payouts/charge-succeeded` with flow control parallelism set to `1`.

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:12-68](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L12-L68), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:76-107](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L76-L107), [apps/web/app/ee/api/stripe/webhook/route.ts:33-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L33-L62)

#### Domain Renewal Execution Flow

1. `POST` receives and verifies the Stripe webhook event, dispatching `"charge.succeeded"` to `chargeSucceeded`.
2. `chargeSucceeded` updates the matching invoice and evaluates `invoice.type === "domainRenewal"`, which invokes `processDomainRenewalInvoice`.
3. `processDomainRenewalInvoice` parses registered domain slugs using `parseRegisteredDomainSlugs` and checks if it represents a registration invoice via `isDomainRegistrationInvoice`. If true, it finalizes the registration via `finalizePremiumDomainRegistration`. Otherwise, it queries `prisma.registeredDomain` and dispatches a renewal success message to QStash targeting `/api/cron/domains/renewal-succeeded`.

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:12-74](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L12-L74), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:109-152](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L109-L152), [apps/web/app/ee/api/stripe/webhook/route.ts:33-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L33-L62)

```mermaid
sequenceDiagram
    participant Route as route.ts
    participant CS as charge-succeeded.ts
    participant DB as Prisma DB
    participant QS as QStash / Cron

    Route->>CS: chargeSucceeded(event)
    CS->>DB: prisma.invoice.findUnique(invoiceId)
    DB-->>CS: return invoice
    CS->>DB: prisma.invoice.update(status: completed)
    alt invoice.type === partnerPayout
        CS->>CS: processPayoutInvoice()
        CS->>DB: prisma.payout.count()
        CS->>QS: qstash.publishJSON(payouts endpoint)
    else invoice.type === domainRenewal
        CS->>CS: processDomainRenewalInvoice()
        CS->>DB: isDomainRegistrationInvoice / findMany
        CS->>QS: qstash.publishJSON(renewal endpoint)
    end
    QS-->>CS: return messageId response
```

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:12-152](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L12-L152), [apps/web/app/ee/api/stripe/webhook/route.ts:60-62](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L60-L62)

### Invoice Processing and Domain Operations

| Invoice Type / Helper | Target Function / Action | QStash Endpoint / Target | Sources |
| :--- | :--- | :--- | :--- |
| **`partnerPayout`** | `processPayoutInvoice` | `${APP_DOMAIN_WITH_NGROK}/api/cron/payouts/charge-succeeded` | [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L67-L68), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L90-L91) |
| **`domainRenewal` (Registration)** | `finalizePremiumDomainRegistration` | Direct function execution (`slugs[0]`) | [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L69-L70), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L118-L121) |
| **`domainRenewal` (Standard)** | `processDomainRenewalInvoice` | `${APP_DOMAIN_WITH_NGROK}/api/cron/domains/renewal-succeeded` | [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L69-L70), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L140-L141) |

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:67-71](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L67-L71), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:90-91](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L90-L91), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:118-121](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L118-L121), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:140-141](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L140-L141)

> [!NOTE]
> When a successful charge lacks a `transfer_group` but includes a Stripe customer ID, the system queries the project table for a matching `stripeId`. If `paymentFailedAt` is populated on that workspace, it is automatically reset to `null` to clear previous billing failure states.

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:17-37](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L17-L37)

### Design Trade-Offs in Charge Processing

| Design Choice | Benefit | Cost | Sources |
| :--- | :--- | :--- | :--- |
| **Transfer Group as Invoice ID** | Links incoming Stripe charge events directly to internal invoice primary keys without metadata lookups. | Relies strictly on Stripe transfer group population during charge creation. | [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L15-L15) |
| **QStash Job Offloading** | Asynchronously decouples downstream payout and domain renewal execution from the webhook response cycle. | Introduces external queue dependency and potential delay in asynchronous fulfillment. | [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L90-L90), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L139-L139) |
| **Idempotent Status Checks** | Prevents duplicate processing if Stripe retries the `charge.succeeded` webhook event. | Requires maintaining explicit status checks on the database invoice record. | [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L51-L53) |

Sources: [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:15-15](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L15-L15), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:51-53](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L51-L53), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:90-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L90-L90), [apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:139-139](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts#L139-L139)

## Payment Failures and Fraud Prevention

### Overview

Payment failures and fraudulent charges are handled through dedicated webhook handlers, automated cron retry routines, and proactive edge blocklist enforcement. When an invoice payment fails (`invoice.payment_failed`), the system retrieves the workspace via `stripeId`, updates `paymentFailedAt` to the current timestamp, and sends branded notification emails through `@dub/email` using the `FailedPayment` template. The email subject line dynamically adjusts based on the attempt count (1st notice, 2nd notice, 3rd notice, or Final notice).

Sources: [apps/web/app/ee/api/stripe/webhook/invoice-payment-failed.tsx:6-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/invoice-payment-failed.tsx#L6-L89)

For domain renewal invoices that fail, a dedicated cron route (`POST /api/cron/invoices/retry-failed`) parses the `invoiceId` using Zod, validates that the invoice status is `"failed"`, checks that `failedAttempts` is strictly less than 3, and ensures the invoice type is specifically `"domainRenewal"`. If an Acme workspace is involved, it resolves the associated Dub workspace Stripe ID before invoking `createPaymentIntent` with an idempotency key combining the invoice ID and attempt count.

Sources: [apps/web/app/ee/api/cron/invoices/retry-failed/route.ts:13-91](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/invoices/retry-failed/route.ts#L13-L91)

### Fraud Detection and Charge Disputes

When a charge dispute is created (`charge_dispute.created`), the system retrieves the associated charge and workspace, disables workspace links, downgrades all users except `LEGAL_USER_ID` to the `"viewer"` role, upserts `LEGAL_USER_ID` as the workspace owner, marks the project as disabled, adds customer details to Stripe fraud value lists, and cancels the subscription.

Sources: [apps/web/app/ee/api/stripe/webhook/charge-dispute-created.ts:9-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/charge-dispute-created.ts#L9-L94)

Similarly, failed charges with an outcome risk level of `"highest"` trigger `detectAndHandleFraudulentFailedCharge`. If no project exists for the customer ID, the customer and card fingerprint are added to Stripe fraud value lists, and any free workspaces owned by the user are either deleted (if they have no links) or disabled with ownership transferred to `LEGAL_USER_ID`. If the user has no paid workspaces, their account is deleted and their email is added to the edge configuration blocklist.

Sources: [apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts:9-157](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts#L9-L157)

> [!WARNING]
> During fraudulent charge handling, if a workspace associated with a fraudulent user is found to be on a paid plan (`workspace.plan !== "free"`), the system skips automated workspace deletion or link disabling, logs an error, and prevents the user deletion step if any paid workspaces remain.

Sources: [apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts:71-79](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts#L71-L79), [apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts:131-131](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts#L131-L131)

## Partner Commissions and Integration Webhooks

### Overview

Partner commission attribution and integration webhook routing manage external partner revenue sharing, promotional code updates, and connected Stripe account synchronization. The primary integration webhook endpoint (`POST /api/stripe/integration/webhook`) accepts signed Stripe events across live, test, and sandbox modes, extracting headers via `Stripe-Signature` and verifying payloads using workspace-specific webhook secrets.

Sources: [apps/web/app/ee/api/stripe/integration/webhook/route.ts:35-55](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L35-L55)

The integration dispatcher filters events against a strict set of relevant event types before resolving the target workspace via `prisma.project.findUnique` using the `stripeConnectId` present on the event account.

Sources: [apps/web/app/ee/api/stripe/integration/webhook/route.ts:22-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts:107-119](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L107-L119)

### Integration Event Routing

| Event Type | Handler / Action | Target Workspace Resolution | Sources |
| :--- | :--- | :--- | :--- |
| `account.application.deauthorized` | `accountApplicationDeauthorized` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L133-L139) |
| `charge.refunded` | `chargeRefunded` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L140-L146) |
| `checkout.session.completed` | `checkoutSessionCompleted` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L147-L153) |
| `coupon.deleted` | `couponDeleted` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L154-L159) |
| `customer.created` / `customer.updated` | `syncCustomer` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L160-L166) |
| `customer.subscription.created` | `customerSubscriptionCreated` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L167-L173) |
| `customer.subscription.deleted` | `customerSubscriptionDeleted` | Event payload extraction | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L174-L176) |
| `invoice.paid` | `invoicePaid` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L177-L183) |
| `promotion_code.updated` | `promotionCodeUpdated` | `prisma.project.findUnique({ where: { stripeConnectId: event.account } })` | [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L184-L189) |

Sources: [apps/web/app/ee/api/stripe/integration/webhook/route.ts:22-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L22-L33), [apps/web/app/ee/api/stripe/integration/webhook/route.ts:133-190](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L133-L190)

### Invoice Paid Attribution and Processing

When an `invoice.paid` event arrives, `invoicePaid` performs multi-tier customer resolution and attribution. It first queries the `Customer` table by `stripeCustomerId`. If not found, it retrieves the connected customer from Stripe, extracts `dubCustomerExternalId` from metadata, and updates the customer record. If still unresolved, it attempts partner promotion code attribution via the invoice.

Sources: [apps/web/app/ee/api/stripe/integration/webhook/invoice-paid.ts:27-119](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts#L27-L119)

> [!NOTE]
> If an invoice sale amount is less than or equal to zero, or if the invoice ID has already been logged within Upstash Redis using a 7-day TTL, the event processing is safely skipped to prevent duplicate commission attribution.

Sources: [apps/web/app/ee/api/stripe/integration/webhook/invoice-paid.ts:128-163](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts#L128-L163)

### Promotion Code Updates

The `promotionCodeUpdated` handler manages promotional discounts linked to Dub partner programs. When a Stripe promotion code update event occurs, if the promotion code becomes inactive, the system queries `prisma.discountCode` for the matching code within the workspace's `defaultProgramId` and deletes the discount code record.

Sources: [apps/web/app/ee/api/stripe/integration/webhook/promotion-code-updated.ts:6-53](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/promotion-code-updated.ts#L6-L53)

## Related

- [Payout Processing](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/payout-processing)
- [Conversion and Event Tracking](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/conversion-and-event-tracking)


## Sitemap

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