---
title: "Webhooks and Postbacks"
description: "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 outb..."
last_updated: "2026-10-05T05:07:35.182658+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/automation-and-communications/webhooks-and-postbacks"
---

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

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

- [apps/web/lib/webhook/schemas.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/schemas.ts)
- [apps/web/lib/postback/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/constants.ts)
- [apps/web/app/ee/api/appsflyer/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.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/ee/api/stripe/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts)
- [apps/web/lib/postback/schemas.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/schemas.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/api/dub/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/route.ts)
- [apps/web/lib/webhook/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/constants.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/ee/api/partner-profile/postbacks/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/postbacks/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/partners.dub.co/dashboard/profile/postbacks/postbackId/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/%5BpostbackId%5D/page.tsx)
- [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/api/webhooks/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/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/scripts/dev/data.json](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/data.json)
- [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/ee/partners.dub.co/dashboard/profile/postbacks/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/page.tsx)
- [apps/web/lib/analytics/types.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/types.ts)
- [apps/web/app/ee/api/paypal/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/paypal/webhook/route.ts)
- [apps/web/app/api/postbacks/callback/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/postbacks/callback/route.ts)
- [apps/web/lib/zod/schemas/analytics-response.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/analytics-response.ts)
- [apps/web/lib/postback/postback-adapters.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/postback-adapters.ts)
- [apps/web/lib/analytics/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/constants.ts)
- [apps/web/ui/partners/format-reward-description.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/format-reward-description.ts)
- [apps/web/app/api/dub/webhook/sale-created.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/sale-created.ts)
- [apps/web/lib/partner-referrals/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/constants.ts)
- [apps/web/ui/partners/program-reward-description.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/program-reward-description.tsx)
- [apps/web/scripts/dev/simulate-shopify-conversion.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/simulate-shopify-conversion.ts)
</details>

## Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/route.ts#L34-L105), [apps/web/app/api/webhooks/callback/route.ts:22-104](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/callback/route.ts#L22-L104), [apps/web/app/ee/api/partner-profile/postbacks/route.ts:41-86](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/postbacks/route.ts#L41-L86)

## Webhook Configuration and Lifecycle Management

### Overview

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

Sources: [apps/web/app/api/webhooks/route.ts:18-32](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/route.ts#L18-L32), [apps/web/app/api/webhooks/route.ts:35-105](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/route.ts#L35-L105)

### Provisioning Call-Chain Execution Walkthrough

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

1. **`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.
2. **`parseRequestBody()`** and **`createWebhookSchema.parse()`** extract and validate the raw request body against the Zod creation schema.
3. **`validateWebhook()`** executes business rules and input checks against the workspace and user session.
4. **`identifyWebhookReceiver()`** inspects the target URL to determine if the destination is an integrated receiver such as Zapier (`WebhookReceiver.zapier`).
5. **`prisma.installedIntegration.findFirst()`** queries database records for installed integrations when Zapier is identified, matching the workspace ID and `ZAPIER_INTEGRATION_ID`.
6. **`createWebhook()`** provisions the webhook entity in the database with the provided name, URL, triggers, link scope, link IDs, folder IDs, and installation ID.
7. **`sendEmail()`** executes asynchronously via Vercel's `waitUntil` utility, dispatching a confirmation notification using the `WebhookAdded` email template.
8. **`NextResponse.json()`** serializes the created webhook using `WebhookSchema.parse()` and returns HTTP status `201`.

Sources: [apps/web/app/api/webhooks/route.ts:35-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/route.ts#L35-L100)

### Webhook Constants and Failure Thresholds

The webhook subsystem defines strict operational thresholds and identifier prefixes to govern endpoint reliability and automatic disabling.

| Constant Name | Value | Description |
| :--- | :--- | :--- |
| `WEBHOOK_SECRET_LENGTH` | `16` | Length of the generated webhook signing secret. |
| `WEBHOOK_ID_PREFIX` | `wh_` | Prefix string for webhook entity identifiers. |
| `WEBHOOK_SECRET_PREFIX` | `whsec_` | Prefix string for webhook signing secrets. |
| `WEBHOOK_EVENT_ID_PREFIX` | `evt_` | Prefix string for dispatched webhook event identifiers. |
| `WEBHOOK_FAILURE_NOTIFY_THRESHOLDS` | `[5, 10, 15]` | Consecutive failure counts that trigger notification alerts. |
| `WEBHOOK_FAILURE_DISABLE_THRESHOLD` | `20` | Consecutive failure count that automatically disables the webhook. |
| `MAX_WEBHOOK_FOLDERS` | `100` | Maximum number of folders allowed for scoping webhooks. |

Sources: [apps/web/lib/webhook/constants.ts:3-65](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/constants.ts#L3-L65)

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

Sources: [apps/web/lib/webhook/constants.ts:62-63](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/constants.ts#L62-L63)

### Workspace and Program Event Triggers

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.

| Trigger Category | Event Identifiers |
| :--- | :--- |
| `WORKSPACE_LEVEL_WEBHOOK_TRIGGERS` | `link.created`, `link.updated`, `link.deleted`, `link.clicked`, `lead.created`, `sale.created` |
| `PROGRAM_LEVEL_WEBHOOK_TRIGGERS` | `partner.application_submitted`, `partner.enrolled`, `partner.merged`, `commission.created`, `bounty.created`, `bounty.updated`, `payout.confirmed`, `discount_code.created`, `discount_code.deleted` |

Sources: [apps/web/lib/webhook/constants.ts:13-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/constants.ts#L13-L32)

## Outbound Delivery Pipeline and Callback Tracking

### Overview

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.

Sources: [apps/web/lib/postback/postback-adapters.ts:15-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/postback-adapters.ts#L15-L68)

### Postback Execution Call Chain

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

1. **`PostbackAdapter.execute()`** receives a `PostbackPayload` containing the event identifier, trigger type, creation timestamp, and raw data.
2. **`this.eventTransformers.transform()`** maps the payload to the specific adapter format, returning immediately if transformation yields no result.
3. **`buildCallbackUrl()`** constructs the destination URL for QStash status tracking, appending query parameters for `postbackId`, `eventId`, and `event`.
4. **`createWebhookSignature()`** computes a cryptographic signature using the postback secret and the transformed payload.
5. **`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"`.
6. **`POST()`** in `/api/webhooks/callback` acts as the webhook status listener for QStash callbacks.
7. **`verifyQstashSignature()`** validates the incoming QStash signature against the raw request body.
8. **`webhookCallbackSchema.parse()`** and `searchParamsSchema.parse()` extract and validate callback body attributes (`url`, `status`, `body`, `sourceBody`, `sourceMessageId`) and query parameters (`webhookId`, `eventId`, `event`, `failed`).
9. **`prisma.webhook.findUnique()`** queries the database to locate the associated webhook entity by its identifier.
10. **`Promise.allSettled()`** executes concurrent post-delivery operations including event recording, failure handling, and payout processing.
11. **`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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/callback/route.ts#L22-L104), [apps/web/lib/postback/postback-adapters.ts:24-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/postback-adapters.ts#L24-L68)

### Callback Processing and Failure Handling

The callback handler evaluates delivery status codes to manage consecutive failures, automated cleanups, and external payout event statuses.

| Condition / Status | Action Taken |
| :--- | :--- |
| `status === 410` and receiver is `zapier` with installation ID | Deletes the webhook entity via `prisma.webhook.delete()` and uninstalls the Zapier webhook. |
| `status >= 400` or `status === -1` | Treats delivery as failed (`isFailed = true`), invokes `handleWebhookFailure(webhookId)`, and maps status `-1` to HTTP `503` for logging. |
| `webhook.consecutiveFailures > 0` and `!isFailed` | Resets the webhook failure count via `resetWebhookFailureCount(webhookId)`. |
| `event === "payout.confirmed"` | Invokes `handleExternalPayoutEvent()` with payload status mapped to `"failure"` (if delivery failed), `"temporary_failure"` (if `isFailed`), or `"success"`. |

Sources: [apps/web/app/api/webhooks/callback/route.ts:49-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/callback/route.ts#L49-L100)

> [!NOTE]
> QStash status callbacks arriving with an HTTP status of `-1` are normalized to HTTP status `503` before being recorded in Tinybird via `recordWebhookEvent`.

Sources: [apps/web/app/api/webhooks/callback/route.ts:72](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/callback/route.ts#L72)

> [!WARNING]
> Zapier webhook endpoints returning a `410` Gone status trigger automatic deletion of the webhook record from the database to clean up stale integrations.

Sources: [apps/web/app/api/webhooks/callback/route.ts:52-64](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/webhooks/callback/route.ts#L52-L64)

## Partner Postback Integration and Adapters

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/postbacks/route.ts#L21-L86), [apps/web/lib/postback/postback-adapters.ts:8-69](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/postback-adapters.ts#L8-L69)

### Creation and Execution Call Chain

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

1. **`PostbackAdapter.execute()`** accepts a `PostbackPayload` containing the event identifier, trigger type, timestamp, and raw data.
2. **`this.eventTransformers.transform()`** transforms the payload format, returning early if no valid transformation is produced.
3. **`buildCallbackUrl()`** constructs the destination tracking URL by appending `postbackId`, `eventId`, and `event` query parameters to the base callback endpoint.
4. **`createWebhookSignature()`** computes a cryptographic signature utilizing the postback secret and transformed payload.
5. **`qstash.publishJSON()`** sends the request to the target URL via QStash, passing headers `"Dub-Signature"` and `"Upstash-Hide-Headers": "true"`.

Sources: [apps/web/lib/postback/postback-adapters.ts:24-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/postback-adapters.ts#L24-L68)

### Configuration and Schema Constants

Postback validation rules, supported event triggers, and database schemas are strictly defined to govern incoming requests and partner profile creation payloads.

| Constant / Schema | Value / Structure | Purpose |
| :--- | :--- | :--- |
| `POSTBACK_SECRET_LENGTH` | `16` | Character length for generated postback secrets. |
| `POSTBACK_SECRET_PREFIX` | `"pbsec_"` | String prefix prepended to all generated postback secrets. |
| `POSTBACK_EVENT_ID_PREFIX` | `"evt_"` | String prefix prepended to postback event identifiers. |
| `MAX_POSTBACKS` | `5` | Maximum number of active postbacks permitted per partner profile. |
| `POSTBACK_TRIGGERS` | `["lead.created", "sale.created", "commission.created"]` | Supported event triggers for affiliate postbacks. |

Sources: [apps/web/lib/postback/constants.ts:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/constants.ts#L1-L20), [apps/web/lib/postback/schemas.ts:10-124](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/schemas.ts#L10-L124)

> [!IMPORTANT]
> The target URL provided during partner postback creation is strictly validated via `parseUrlSchema` to require the HTTPS protocol.

Sources: [apps/web/lib/postback/schemas.ts:26-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/schemas.ts#L26-L28)

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

Sources: [apps/web/app/ee/api/partner-profile/postbacks/route.ts:48-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/postbacks/route.ts#L48-L59)

## Internal Dub Event Dispatch Receivers

### Overview

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.

Sources: [apps/web/app/api/dub/webhook/route.ts:8-40](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/route.ts#L8-L40), [apps/web/lib/webhook/schemas.ts:64-208](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/schemas.ts#L64-L208)

### Execution Call Chain and Request Verification

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

1. **`POST /api/dub/webhook`** receives the raw HTTP request and parses the body via `req.json()`.
2. **`webhookPayloadSchema.parse(body)`** validates the structure against the base payload schema, extracting the `event` type and payload `data`.
3. **`req.headers.get("Dub-Signature")`** extracts the signature header, returning a `401` status response if no signature is provided.
4. **`crypto.createHmac()`** computes an expected SHA-256 HMAC digest of the stringified request body utilizing the secret stored in `process.env.DUB_WEBHOOK_SECRET`.
5. **`timingSafeCompare()`** compares the incoming header signature with the computed signature, returning a `400` status response if verification fails.
6. **`leadCreated()`** or **`saleCreated()`** executes based on the matched `event` switch case, returning an `"OK"` or status message response.

Sources: [apps/web/app/api/dub/webhook/route.ts:9-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/route.ts#L9-L39)

### Webhook Event Schemas and Handlers

The webhook payload schemas define strict Zod validation structures for inbound event types and associated metadata objects.

| Schema Constant | Base Validation Type | Description / Purpose |
| :--- | :--- | :--- |
| `webhookPayloadSchema` | `z.object` | Validates root webhook shape (`id`, `event`, `createdAt`, `data`). |
| `clickWebhookEventSchema` | `z.object` | Bundles `click` (`clickEventSchema`) and `link` (`linkEventSchema`) objects. |
| `leadWebhookEventSchema` | `z.object` | Validates `eventName`, `customer`, `click`, `link`, optional `partner`, and `metadata`. |
| `saleWebhookEventSchema` | `z.object` | Validates lead properties alongside `sale` (`amount`, `currency`, `paymentProcessor`, `invoiceId`). |

Sources: [apps/web/lib/webhook/schemas.ts:15-61](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/schemas.ts#L15-L61)

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

Sources: [apps/web/app/api/dub/webhook/sale-created.ts:3-8](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/sale-created.ts#L3-L8)

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

Sources: [apps/web/lib/webhook/schemas.ts:27-42](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/schemas.ts#L27-L42)

## Inbound Partner and Provider Webhooks

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L27-L177), [apps/web/app/ee/api/singular/webhook/route.ts:38-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L38-L113), [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/hubspot/webhook/route.ts:12-73](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/hubspot/webhook/route.ts#L12-L73), [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/connect/v2/webhook/route.ts:24-96](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/v2/webhook/route.ts#L24-L96), [apps/web/app/ee/api/intercom/webhook/route.ts:12-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/webhook/route.ts#L12-L45), [apps/web/app/ee/api/paypal/webhook/route.ts:21-73](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/paypal/webhook/route.ts#L21-L73)

### Execution Call Chain and Security Validation

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

1. **`req.text()` or `getSearchParams()`** reads the raw body payload or URL search parameters depending on whether the provider delivers JSON postbacks or query parameters.
2. **`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.
3. **`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`).
4. **`prisma.project.findUnique()`** or workspace lookups locate the corresponding project workspace linked to the external identifier or Stripe Connect ID (`event.account`).
5. **Event handler execution** routes the parsed event through a `switch` statement or enqueues batch jobs via QStash for asynchronous processing.

Sources: [apps/web/app/ee/api/appsflyer/webhook/route.ts:37-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L37-L89), [apps/web/app/ee/api/singular/webhook/route.ts:40-91](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L40-L91), [apps/web/app/ee/api/stripe/webhook/route.ts:39-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/webhook/route.ts#L39-L48), [apps/web/app/ee/api/stripe/integration/webhook/route.ts:57-70](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L57-L70), [apps/web/app/ee/api/hubspot/webhook/route.ts:15-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/hubspot/webhook/route.ts#L15-L45), [apps/web/app/ee/api/stripe/connect/webhook/route.ts:28-52](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/webhook/route.ts#L28-L52), [apps/web/app/ee/api/stripe/connect/v2/webhook/route.ts:28-55](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/v2/webhook/route.ts#L28-L55), [apps/web/app/ee/api/intercom/webhook/route.ts:14](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/webhook/route.ts#L14), [apps/web/app/ee/api/paypal/webhook/route.ts:26-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/paypal/webhook/route.ts#L26-L33)

### Supported Inbound Webhook Routes and Event Sets

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.

| Route Path | Validation Mechanism | Supported Events / Topics |
| :--- | :--- | :--- |
| `/api/stripe/webhook` | `Stripe-Signature` header & SDK construction | `charge.succeeded`, `charge.failed`, `charge.refunded`, `charge.dispute.created`, `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`, `payment_intent.requires_action`, `transfer.reversed` |
| `/api/stripe/integration/webhook` | `Stripe-Signature` header & mode-specific secrets | `account.application.deauthorized`, `charge.refunded`, `checkout.session.completed`, `coupon.deleted`, `customer.created`, `customer.updated`, `customer.subscription.created`, `customer.subscription.deleted`, `invoice.paid`, `promotion_code.updated` |
| `/api/stripe/connect/webhook` | `Stripe-Signature` header & `STRIPE_CONNECT_WEBHOOK_SECRET` | `account.application.deauthorized`, `account.external_account.updated`, `account.updated`, `balance.available`, `payout.paid`, `payout.failed` |
| `/api/stripe/connect/v2/webhook` | `Stripe-Signature` header & `STRIPE_CONNECT_V2_WEBHOOK_SECRET` | `v2.core.account.closed`, `v2.core.account[configuration.recipient].updated`, `v2.core.account[configuration.recipient].capability_status_updated`, `v2.money_management.outbound_payment.posted`, `v2.money_management.outbound_payment.returned`, `v2.money_management.outbound_payment.failed` |
| `/api/appsflyer/webhook` | IP range check (`APPSFLYER_IP_RANGES`) & query schema parsing | `lead`, `sale` (via `partnerEventId` query parameter) |
| `/api/singular/webhook` | IP range check (`SINGULAR_IP_RANGES`) & `dub_workspace_id` validation | `activated`, `sng_complete_registration`, `sng_subscribe`, `sng_ecommerce_purchase`, `__iap__`, `Copy GAID`, `copy IDFA` |
| `/api/hubspot/webhook` | `X-HubSpot-Signature` header & SHA-256 HMAC comparison | Array of webhook event objects (fanned out via QStash) |
| `/api/intercom/webhook` | HMAC signature verification (`verifyIntercomWebhookSignature`) | `conversation.admin.replied`, `ping` |
| `/api/paypal/webhook` | Signature verification (`verifySignature`) | `PAYMENT.PAYOUTS-ITEM.SUCCEEDED`, `PAYMENT.PAYOUTS-ITEM.BLOCKED`, `PAYMENT.PAYOUTS-ITEM.CANCELED`, `PAYMENT.PAYOUTS-ITEM.DENIED`, `PAYMENT.PAYOUTS-ITEM.FAILED`, `PAYMENT.PAYOUTS-ITEM.HELD`, `PAYMENT.PAYOUTS-ITEM.REFUNDED`, `PAYMENT.PAYOUTS-ITEM.RETURNED`, `PAYMENT.PAYOUTS-ITEM.UNCLAIMED` |

Sources: [apps/web/app/ee/api/appsflyer/webhook/route.ts:20-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts#L20-L59), [apps/web/app/ee/api/singular/webhook/route.ts:15-35](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts#L15-L35), [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/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/hubspot/webhook/route.ts:9-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/hubspot/webhook/route.ts#L9-L48), [apps/web/app/ee/api/stripe/connect/webhook/route.ts:12-19](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/webhook/route.ts#L12-L19), [apps/web/app/ee/api/stripe/connect/v2/webhook/route.ts:12-19](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/connect/v2/webhook/route.ts#L12-L19), [apps/web/app/ee/api/intercom/webhook/route.ts:9-17](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/webhook/route.ts#L9-L17), [apps/web/app/ee/api/paypal/webhook/route.ts:7-18](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/paypal/webhook/route.ts#L7-L18)

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

Sources: [apps/web/app/ee/api/stripe/integration/webhook/route.ts:193-204](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/route.ts#L193-L204)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/hubspot/webhook/route.ts#L54-L62), [apps/web/app/ee/api/intercom/webhook/route.ts:27-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/webhook/route.ts#L27-L34)

## Partner Portal Postback Administration

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/page.tsx#L1-L97), [apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/postbackId/page.tsx:1-158](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/%5BpostbackId%5D/page.tsx#L1-L158)

### Constants and Configuration

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.

| Constant Name | Value / Format | Description |
| --- | --- | --- |
| `POSTBACK_SECRET_LENGTH` | `16` | The length of generated cryptographic postback secrets |
| `POSTBACK_SECRET_PREFIX` | `"pbsec_"` | Prefix string appended to postback signing secrets |
| `POSTBACK_EVENT_ID_PREFIX` | `"evt_"` | Prefix string appended to logged postback event IDs |
| `MAX_POSTBACKS` | `5` | Maximum number of active postbacks permitted per partner profile |
| `POSTBACK_TRIGGERS` | `lead.created`, `sale.created`, `commission.created` | Array of valid event trigger types supported by postbacks |

Sources: [apps/web/lib/postback/constants.ts:1-19](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/constants.ts#L1-L19)

### Component Views and State Lifecycle

#### Postback List View (`/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`.

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/page.tsx:18-97](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/page.tsx#L18-L97)

#### Postback Detail View (`/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`. 

```typescript
  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 },
  );
```

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/postbackId/page.tsx:27-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/%5BpostbackId%5D/page.tsx#L27-L45)

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.

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/postbackId/page.tsx:56-58](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/%5BpostbackId%5D/page.tsx#L56-L58)

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

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/postbacks/postbackId/page.tsx:56-58](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/%5BpostbackId%5D/page.tsx#L56-L58)

## Related

- [Conversion and Event Tracking](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/conversion-and-event-tracking)
- [Background Jobs and Queues](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/background-jobs-and-queues)


## Sitemap

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