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:
Webhooks and postbacks facilitate real-time event-driven integrations by securely transmitting data between Dub workspaces, external partners, and third-party services. The system handles both outbound webhook dispatching through queue pipelines and inbound webhook ingestion from external payment gateways, attribution networks, and partner applications, ensuring reliable event delivery, signature verification, and delivery failure tracking.
Sources: apps/web/app/api/webhooks/route.ts:34-105, apps/web/app/api/webhooks/callback/route.ts:22-104, apps/web/app/ee/api/partner-profile/postbacks/route.ts:41-86
Workspace webhooks are provisioned and managed through public API routes residing at /api/webhooks, enforcing workspace-level permissions and plan requirements. The management lifecycle spans retrieving existing webhook configurations via GET and provisioning new endpoints via POST. Every webhook operation validates incoming payloads using Zod schemas, checks required permissions such as webhooks.read and webhooks.write, and restricts access to specific subscription plans including business, advanced, and enterprise.
The creation of a new workspace webhook follows an explicit execution flow through the public API route handler. When a POST request is received, the operation proceeds through the following call chain:
withWorkspace() → parseRequestBody() → createWebhookSchema.parse() → validateWebhook() → identifyWebhookReceiver() → prisma.installedIntegration.findFirst() → createWebhook() → sendEmail() → NextResponse.json()
withWorkspace() resolves and validates the workspace from the session context, enforcing that the caller possesses the webhooks.write permission and belongs to a business, advanced, or enterprise subscription plan.parseRequestBody() and createWebhookSchema.parse() extract and validate the raw request body against the Zod creation schema.validateWebhook() executes business rules and input checks against the workspace and user session.identifyWebhookReceiver() inspects the target URL to determine if the destination is an integrated receiver such as Zapier (WebhookReceiver.zapier).prisma.installedIntegration.findFirst() queries database records for installed integrations when Zapier is identified, matching the workspace ID and ZAPIER_INTEGRATION_ID.createWebhook() provisions the webhook entity in the database with the provided name, URL, triggers, link scope, link IDs, folder IDs, and installation ID.sendEmail() executes asynchronously via Vercel's waitUntil utility, dispatching a confirmation notification using the WebhookAdded email template.NextResponse.json() serializes the created webhook using WebhookSchema.parse() and returns HTTP status 201.The webhook subsystem defines strict operational thresholds and identifier prefixes to govern endpoint reliability and automatic disabling.
Warning
Reaching the WEBHOOK_FAILURE_DISABLE_THRESHOLD of 20 consecutive delivery failures will automatically disable the webhook endpoint. Operators must monitor failure notification thresholds at 5, 10, and 15 failures to prevent automatic disabling of critical integrations.
Webhook triggers are divided into workspace-level and program-level event categories. Workspace triggers govern core link and conversion actions, while program-level triggers manage partner ecosystems, bounties, payouts, and discount codes.
The outbound delivery pipeline dispatches events via QStash, generating cryptographic signatures and handling status tracking through callback endpoints. The PostbackAdapter class handles event transformation, search parameter construction, signature generation, and QStash publishing.
The postback dispatch and callback tracking pipeline proceeds through the following call chain:
PostbackAdapter.execute() → this.eventTransformers.transform() → buildCallbackUrl() → createWebhookSignature() → qstash.publishJSON() → POST() → verifyQstashSignature() → webhookCallbackSchema.parse() → prisma.webhook.findUnique() → Promise.allSettled() → recordWebhookEvent()
PostbackAdapter.execute() receives a PostbackPayload containing the event identifier, trigger type, creation timestamp, and raw data.this.eventTransformers.transform() maps the payload to the specific adapter format, returning immediately if transformation yields no result.buildCallbackUrl() constructs the destination URL for QStash status tracking, appending query parameters for postbackId, eventId, and event.createWebhookSignature() computes a cryptographic signature using the postback secret and the transformed payload.qstash.publishJSON() dispatches the HTTP request to the target URL via QStash, configuring both callback and failureCallback properties to point to the generated callback URL with headers "Dub-Signature" and "Upstash-Hide-Headers": "true".POST() in /api/webhooks/callback acts as the webhook status listener for QStash callbacks.verifyQstashSignature() validates the incoming QStash signature against the raw request body.webhookCallbackSchema.parse() and searchParamsSchema.parse() extract and validate callback body attributes (url, status, body, sourceBody, sourceMessageId) and query parameters (webhookId, eventId, event, failed).prisma.webhook.findUnique() queries the database to locate the associated webhook entity by its identifier.Promise.allSettled() executes concurrent post-delivery operations including event recording, failure handling, and payout processing.recordWebhookEvent() logs the event details to Tinybird with the request body, response body, HTTP status, and message ID.Sources: apps/web/app/api/webhooks/callback/route.ts:22-104, apps/web/lib/postback/postback-adapters.ts:24-68
The callback handler evaluates delivery status codes to manage consecutive failures, automated cleanups, and external payout event statuses.
Note
QStash status callbacks arriving with an HTTP status of -1 are normalized to HTTP status 503 before being recorded in Tinybird via recordWebhookEvent.
Warning
Zapier webhook endpoints returning a 410 Gone status trigger automatic deletion of the webhook record from the database to clean up stale integrations.
Partner postbacks allow affiliates and partners to receive real-time HTTP notifications for conversion events. The system governs postback provisioning through partner profile API endpoints that enforce validation constraints, channel receiver identification, and payload adapters.
Sources: apps/web/app/ee/api/partner-profile/postbacks/route.ts:21-86, apps/web/lib/postback/postback-adapters.ts:8-69
Dispatching partner postback events proceeds through a specific execution order involving abstract adapter execution, event transformation, URL construction, signature creation, and QStash publishing:
PostbackAdapter.execute() → this.eventTransformers.transform() → buildCallbackUrl() → createWebhookSignature() → qstash.publishJSON()
PostbackAdapter.execute() accepts a PostbackPayload containing the event identifier, trigger type, timestamp, and raw data.this.eventTransformers.transform() transforms the payload format, returning early if no valid transformation is produced.buildCallbackUrl() constructs the destination tracking URL by appending postbackId, eventId, and event query parameters to the base callback endpoint.createWebhookSignature() computes a cryptographic signature utilizing the postback secret and transformed payload.qstash.publishJSON() sends the request to the target URL via QStash, passing headers "Dub-Signature" and "Upstash-Hide-Headers": "true".Postback validation rules, supported event triggers, and database schemas are strictly defined to govern incoming requests and partner profile creation payloads.
Important
The target URL provided during partner postback creation is strictly validated via parseUrlSchema to require the HTTPS protocol.
Warning
Partner profile endpoints enforce a hard cap of MAX_POSTBACKS (5) per partner; attempting to create additional postbacks throws an exceeded_limit API error.
The internal webhook intake API routes incoming Dub internal webhook events, validates cryptographic signatures, and dispatches payloads to specific conversion handlers. The receiver endpoint processes core lifecycle events including link clicks, lead captures, and sale conversions.
Incoming webhook requests flow through a strictly ordered intake and verification sequence before reaching event handlers:
POST /api/dub/webhook → req.json() → webhookPayloadSchema.parse() → req.headers.get("Dub-Signature") → crypto.createHmac() → timingSafeCompare() → leadCreated() / saleCreated()
POST /api/dub/webhook receives the raw HTTP request and parses the body via req.json().webhookPayloadSchema.parse(body) validates the structure against the base payload schema, extracting the event type and payload data.req.headers.get("Dub-Signature") extracts the signature header, returning a 401 status response if no signature is provided.crypto.createHmac() computes an expected SHA-256 HMAC digest of the stringified request body utilizing the secret stored in process.env.DUB_WEBHOOK_SECRET.timingSafeCompare() compares the incoming header signature with the computed signature, returning a 400 status response if verification fails.leadCreated() or saleCreated() executes based on the matched event switch case, returning an "OK" or status message response.The webhook payload schemas define strict Zod validation structures for inbound event types and associated metadata objects.
Warning
If a referral link is missing from the sale.created payload data during handler execution, saleCreated() immediately terminates execution and returns a "Referral link not found in webhook payload" string response rather than throwing an unhandled exception.
Tip
The metadataSchema field helper automatically runs coerceJsonString preprocessing on incoming payloads, attempting to parse JSON strings back into record structures while gracefully falling back to raw values if parsing fails.
The inbound partner and provider webhooks subsystem handles external webhook ingestion from payment gateways, customer support tools, and mobile attribution networks. These API routes validate request signatures, verify origin IP ranges, filter out unsupported event types, and fan out or delegate payloads to specialized processors or conversion tracking utilities.
Sources: apps/web/app/ee/api/appsflyer/webhook/route.ts:27-177, apps/web/app/ee/api/singular/webhook/route.ts:38-113, 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/hubspot/webhook/route.ts:12-73, apps/web/app/ee/api/stripe/connect/webhook/route.ts:24-91, apps/web/app/ee/api/stripe/connect/v2/webhook/route.ts:24-96, apps/web/app/ee/api/intercom/webhook/route.ts:12-45, apps/web/app/ee/api/paypal/webhook/route.ts:21-73
Incoming external webhook requests undergo strict validation routines before any business logic executes. Depending on the provider, validation involves cryptographic signature verification, constant-time hash comparisons, or IP range allowlisting:
req.text() / getSearchParams() → req.headers.get("signature") / getIP() → stripe.webhooks.constructEvent() / timingSafeCompare() / isIpInRange() → prisma.project.findUnique() → Event handler execution
req.text() or getSearchParams() reads the raw body payload or URL search parameters depending on whether the provider delivers JSON postbacks or query parameters.req.headers.get() or getIP() extracts authentication signatures (e.g., "Stripe-Signature", "X-HubSpot-Signature") or resolves the client IP address for network-restricted endpoints.stripe.webhooks.constructEvent(), timingSafeCompare(), or isIpInRange() validates the request integrity by checking HMAC signatures against secrets (e.g., STRIPE_WEBHOOK_SECRET, HUBSPOT_CLIENT_SECRET) or evaluating allowed IPCIDR ranges (APPSFLYER_IP_RANGES, SINGULAR_IP_RANGES).prisma.project.findUnique() or workspace lookups locate the corresponding project workspace linked to the external identifier or Stripe Connect ID (event.account).switch statement or enqueues batch jobs via QStash for asynchronous processing.Sources: apps/web/app/ee/api/appsflyer/webhook/route.ts:37-89, apps/web/app/ee/api/singular/webhook/route.ts:40-91, apps/web/app/ee/api/stripe/webhook/route.ts:39-48, apps/web/app/ee/api/stripe/integration/webhook/route.ts:57-70, apps/web/app/ee/api/hubspot/webhook/route.ts:15-45, apps/web/app/ee/api/stripe/connect/webhook/route.ts:28-52, apps/web/app/ee/api/stripe/connect/v2/webhook/route.ts:28-55, apps/web/app/ee/api/intercom/webhook/route.ts:14, apps/web/app/ee/api/paypal/webhook/route.ts:26-33
Each integration endpoint recognizes a distinct set of event types or topic identifiers. Unrecognized events are safely skipped and acknowledged to prevent repeated delivery retries from providers.
Sources: apps/web/app/ee/api/appsflyer/webhook/route.ts:20-59, apps/web/app/ee/api/singular/webhook/route.ts:15-35, apps/web/app/ee/api/stripe/webhook/route.ts:18-30, apps/web/app/ee/api/stripe/integration/webhook/route.ts:22-33, apps/web/app/ee/api/hubspot/webhook/route.ts:9-48, apps/web/app/ee/api/stripe/connect/webhook/route.ts:12-19, apps/web/app/ee/api/stripe/connect/v2/webhook/route.ts:12-19, apps/web/app/ee/api/intercom/webhook/route.ts:9-17, apps/web/app/ee/api/paypal/webhook/route.ts:7-18
Warning
When handling Stripe rate-limit errors (isStripeRateLimitError) within the Stripe integration webhook router, the endpoint returns HTTP status 429 instead of 500. This instructs Stripe to automatically back off and retry the webhook delivery rather than treating the failure as an internal server error.
Tip
High-volume webhook receivers like HubSpot and Intercom parse incoming batches and immediately fan out individual events to QStash queues (e.g., process-hubspot-webhook, process-intercom-webhook). This pattern keeps the primary HTTP ingestion handler fast and prevents a slow or failing payload from blocking the rest of the batch.
Sources: apps/web/app/ee/api/hubspot/webhook/route.ts:54-62, apps/web/app/ee/api/intercom/webhook/route.ts:27-34
Partner Portal Postback Administration provides the user interface views and client-side orchestration layers for partners to register, manage, and inspect HTTP postbacks within the Dub partner ecosystem (partners.dub.co). Partners use these views to configure destination endpoints, manage signing secrets, and inspect real-time delivery event logs when conversion events occur.
Sources: apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/page.tsx:1-97, apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/postbackId/page.tsx:1-158
Postback configuration relies on specific structural and naming constants defined in the core postback library, governing secret formats, event ID prefixes, and trigger event options.
/profile/postbacks)The main postback listing page queries /api/partner-profile/postbacks with SWR (keepPreviousData: true), rendering loading placeholders via PostbackPlaceholder, existing postback cards (PostbackCard), or an EmptyState component when no postbacks are configured. The view exposes an add action button linked to openAddPostbackModal.
/profile/postbacks/[postbackId])The detail view inspects a specific postback by fetching /api/partner-profile/postbacks/{postbackId} and its associated delivery events from /api/partner-profile/postbacks/{postbackId}/events.
const {
data: postback,
error,
isLoading,
mutate,
} = useSWR<PostbackProps>(
postbackId ? `/api/partner-profile/postbacks/${postbackId}` : null,
fetcher,
);
const {
data: events,
error: eventsError,
isLoading: isEventsLoading,
} = useSWR<PostbackEventProps[]>(
postbackId ? `/api/partner-profile/postbacks/${postbackId}/events` : null,
fetcher,
{ keepPreviousData: true },
);If a 404 error is returned by the postback API endpoint (error.status === 404), the page automatically redirects the user back to the main /profile/postbacks route.
Warning
When a postback resource is deleted or returns a 404 Not Found status, client-side error handling intercepts the status code and executes an immediate navigation redirect to /profile/postbacks, preventing stale UI states from persisting in the partner dashboard.