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:
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, apps/web/app/ee/api/stripe/integration/webhook/route.ts:36-228, apps/web/app/ee/api/stripe/connect/webhook/route.ts:24-91, apps/web/app/ee/api/stripe/webhook/charge-dispute-created.ts:9-94, apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts:9-158
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().
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
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.
The table below details all eleven supported events recognized by relevantEvents alongside their corresponding event-handling functions:
Sources: apps/web/app/ee/api/stripe/webhook/route.ts:18-30, apps/web/app/ee/api/stripe/webhook/route.ts:59-93
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.
When processing succeeds without interruption, the route returns the result formatted via logAndRespond(...).
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, apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:80-105
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.
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, apps/web/app/ee/api/stripe/webhook/checkout-session-completed.ts:80-105
Note
Sessions operating in setup mode are explicitly skipped by checkoutSessionCompleted before retrieving subscription records or updating workspace states.
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.
Sources: apps/web/app/ee/api/stripe/webhook/customer-subscription-updated.ts:8-89, apps/web/app/ee/api/stripe/webhook/utils/update-workspace-plan.ts:34-171
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.
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.
Sources: apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:23-100, apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:105-267
Sources: apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:74-100, apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:133-211, apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:210-210, apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:270-298
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, apps/web/app/ee/api/stripe/webhook/customer-subscription-deleted.ts:270-298
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, apps/web/app/ee/api/stripe/webhook/route.ts:60-62
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.
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.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.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, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:76-107, apps/web/app/ee/api/stripe/webhook/route.ts:33-62
POST receives and verifies the Stripe webhook event, dispatching "charge.succeeded" to chargeSucceeded.chargeSucceeded updates the matching invoice and evaluates invoice.type === "domainRenewal", which invokes processDomainRenewalInvoice.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, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:109-152, apps/web/app/ee/api/stripe/webhook/route.ts:33-62
Sources: apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:12-152, apps/web/app/ee/api/stripe/webhook/route.ts:60-62
Sources: apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:67-71, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:90-91, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:118-121, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:140-141
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:15-15, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:51-53, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:90-90, apps/web/app/ee/api/stripe/webhook/charge-succeeded.ts:139-139
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).
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.
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.
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
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, apps/web/app/ee/api/stripe/webhook/utils/detect-and-handle-fraudulent-failed-charge.ts:131-131
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.
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, apps/web/app/ee/api/stripe/integration/webhook/route.ts:107-119
Sources: apps/web/app/ee/api/stripe/integration/webhook/route.ts:22-33, apps/web/app/ee/api/stripe/integration/webhook/route.ts:133-190
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.
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.
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.