Skip to main content

Overview

Webhooks allow you to receive real-time notifications when events occur in your Dub workspace. Instead of polling the API, Dub will send HTTP POST requests to your configured endpoint whenever a subscribed event happens.

Webhook Events

Dub supports the following webhook triggers:

Workspace-Level Events

These events can be configured at the workspace level and apply to all links:
EventDescription
link.createdTriggered when a new link is created
link.updatedTriggered when a link is updated
link.deletedTriggered when a link is deleted
lead.createdTriggered when a lead conversion is tracked
sale.createdTriggered when a sale conversion is tracked
These events are configured per-link and only fire for specific links:
EventDescription
link.clickedTriggered when a specific link is clicked

Program-Level Events

These events are for partner program management:
EventDescription
partner.application_submittedTriggered when a partner applies to join a program
partner.enrolledTriggered when a partner is enrolled in a program
commission.createdTriggered when a commission is created for a partner
bounty.createdTriggered when a bounty is created
bounty.updatedTriggered when a bounty is updated
payout.confirmedTriggered when a payout is confirmed

Event Schema

All webhook payloads follow a consistent structure:
lib/webhook/schemas.ts
export const webhookPayloadSchema = z.object({
  id: z.string().describe("Unique identifier for the event."),
  event: z
    .enum(WEBHOOK_TRIGGERS)
    .describe("The type of event that triggered the webhook."),
  createdAt: z
    .string()
    .describe("The date and time when the event was created in UTC."),
  data: z.any().describe("The data associated with the event."),
});

Event-Specific Schemas

Creating a Webhook

Via API

Create a webhook using the Dub API:
const webhook = await dub.webhooks.create({
  name: "Production Webhook",
  url: "https://api.yourapp.com/webhooks/dub",
  triggers: ["lead.created", "sale.created"],
  secret: "whsec_your_secret_key_here", // Optional: Use your own secret
});

Via Dashboard

1

Navigate to Webhooks

Go to your workspace settings and click on the “Webhooks” tab.
2

Create Webhook

Click “Create Webhook” and enter your endpoint URL.
3

Select Triggers

Choose which events should trigger the webhook.
4

Configure Secret

Optionally provide your own signing secret, or let Dub generate one.
5

Save and Test

Save the webhook and use the test feature to verify it’s working.

Webhook Security

Signature Verification

All webhook requests include an HMAC-SHA256 signature in the X-Dub-Signature header. Always verify this signature to ensure requests are from Dub:
lib/webhook/signature.ts
export const createWebhookSignature = async (secret: string, body: any) => {
  const keyData = new TextEncoder().encode(secret);
  const messageData = new TextEncoder().encode(JSON.stringify(body));

  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    keyData,
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );

  const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
  const signatureArray = Array.from(new Uint8Array(signature));
  const hexSignature = signatureArray
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");

  return hexSignature;
};

Verifying Signatures

Implement signature verification in your webhook handler:
import { createHmac } from "crypto";

export async function POST(req: Request) {
  const signature = req.headers.get("X-Dub-Signature");
  const body = await req.text();
  
  // Get your webhook secret from environment variables
  const secret = process.env.DUB_WEBHOOK_SECRET;
  
  // Calculate the expected signature
  const expectedSignature = createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  
  // Compare signatures
  if (signature !== expectedSignature) {
    return new Response("Invalid signature", { status: 401 });
  }
  
  // Process the webhook
  const payload = JSON.parse(body);
  // ... handle the event
  
  return new Response("OK", { status: 200 });
}
Important: Always verify webhook signatures in production. This prevents unauthorized requests from triggering actions in your system.

Webhook Delivery

Retry Policy

Dub uses QStash for reliable webhook delivery with automatic retries:
  • Webhooks are retried up to 3 times with exponential backoff
  • If all retries fail, the webhook is marked as failed
  • You can view failed deliveries in the webhook event log

Timeout

Webhook endpoints must respond within 30 seconds. If your endpoint takes longer:
  1. Return a 200 OK response immediately
  2. Process the event asynchronously in the background
  3. Use a queue system for long-running operations

Failure Notifications

Dub monitors webhook health and sends notifications when failures occur:
lib/webhook/constants.ts
export const WEBHOOK_FAILURE_NOTIFY_THRESHOLDS = [5, 10, 15] as const;
export const WEBHOOK_FAILURE_DISABLE_THRESHOLD = 20 as const;
  • You’ll receive email notifications after 5, 10, and 15 consecutive failures
  • After 20 consecutive failures, the webhook is automatically disabled

Implementation Examples

Next.js Route Handler

app/api/webhooks/dub/route.ts
import { webhookPayloadSchema } from "@dub/api";
import { createHmac } from "crypto";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  try {
    // Verify signature
    const signature = req.headers.get("X-Dub-Signature");
    const body = await req.text();
    
    const expectedSignature = createHmac("sha256", process.env.DUB_WEBHOOK_SECRET!)
      .update(body)
      .digest("hex");
    
    if (signature !== expectedSignature) {
      return NextResponse.json(
        { error: "Invalid signature" },
        { status: 401 }
      );
    }
    
    // Parse and validate payload
    const payload = webhookPayloadSchema.parse(JSON.parse(body));
    
    // Handle different event types
    switch (payload.event) {
      case "lead.created":
        await handleLeadCreated(payload.data);
        break;
      case "sale.created":
        await handleSaleCreated(payload.data);
        break;
      case "partner.enrolled":
        await handlePartnerEnrolled(payload.data);
        break;
      default:
        console.log(`Unhandled event: ${payload.event}`);
    }
    
    return NextResponse.json({ received: true });
  } catch (error) {
    console.error("Webhook error:", error);
    return NextResponse.json(
      { error: "Webhook processing failed" },
      { status: 500 }
    );
  }
}

async function handleLeadCreated(data: any) {
  // Send to CRM
  await fetch("https://your-crm.com/api/leads", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: data.customer.email,
      name: data.customer.name,
      source: data.link.shortLink,
      metadata: {
        click_id: data.click.id,
        country: data.click.country,
        device: data.click.device,
      },
    }),
  });
}

async function handleSaleCreated(data: any) {
  // Record revenue in analytics
  await fetch("https://your-analytics.com/api/revenue", {
    method: "POST",
    body: JSON.stringify({
      customer_id: data.customer.externalId,
      amount: data.sale.amount,
      currency: data.sale.currency,
      attributed_to: data.link.shortLink,
    }),
  });
}

async function handlePartnerEnrolled(data: any) {
  // Send welcome email to partner
  await sendEmail({
    to: data.email,
    subject: "Welcome to our Partner Program!",
    body: `Your unique link: ${data.links[0].shortLink}`,
  });
}

Express.js Handler

import express from "express";
import { createHmac } from "crypto";

const app = express();

app.post("/webhooks/dub", express.raw({ type: "application/json" }), async (req, res) => {
  const signature = req.headers["x-dub-signature"];
  const body = req.body.toString();
  
  // Verify signature
  const expectedSignature = createHmac("sha256", process.env.DUB_WEBHOOK_SECRET)
    .update(body)
    .digest("hex");
  
  if (signature !== expectedSignature) {
    return res.status(401).json({ error: "Invalid signature" });
  }
  
  const payload = JSON.parse(body);
  
  // Process event
  if (payload.event === "sale.created") {
    await processSale(payload.data);
  }
  
  res.json({ received: true });
});
For high-traffic links, you can attach webhooks directly to specific links to receive click events:
const webhook = await dub.webhooks.create({
  name: "Campaign Click Tracker",
  url: "https://api.yourapp.com/webhooks/clicks",
  triggers: ["link.clicked"],
  linkIds: ["link_abc123", "link_xyz789"],
});
Link-level webhooks are cached for performance. Click events on these links will trigger the webhook in real-time.

Testing Webhooks

Using the Dashboard

  1. Navigate to your webhook in the dashboard
  2. Click “Send Test Event”
  3. Select the event type to test
  4. View the response and delivery status

Local Development

Use a tool like ngrok to expose your local server:
ngrok http 3000
Then create a webhook with your ngrok URL:
const webhook = await dub.webhooks.create({
  url: "https://abc123.ngrok.io/api/webhooks/dub",
  triggers: ["lead.created"],
});

Best Practices

Idempotency

Webhooks may be delivered more than once. Use the event id to ensure idempotent processing:
const processedEvents = new Set();

if (processedEvents.has(payload.id)) {
  return NextResponse.json({ received: true }); // Already processed
}

processedEvents.add(payload.id);
// Process event...

Async Processing

Respond quickly and process events asynchronously:
export async function POST(req: Request) {
  const payload = await req.json();
  
  // Queue for background processing
  await queue.add("webhook-event", payload);
  
  // Respond immediately
  return NextResponse.json({ received: true });
}

Error Handling

Log errors but still return a success response to prevent retries:
try {
  await processEvent(payload);
} catch (error) {
  // Log error for investigation
  console.error("Failed to process webhook:", error);
  // Still return success to prevent retries
}

return NextResponse.json({ received: true });

Monitor Webhook Health

Regularly check your webhook logs to identify issues:
  1. Failed deliveries
  2. Slow response times
  3. Validation errors

Troubleshooting

Webhooks Not Received

1

Check Endpoint URL

Verify your endpoint URL is publicly accessible and returns 200 OK.
2

Review Event Logs

Check the webhook event log in your Dub dashboard for delivery errors.
3

Verify Triggers

Ensure the webhook is configured for the events you expect to receive.
4

Test Signature Verification

Temporarily disable signature verification to rule out authentication issues.

Signature Verification Failed

  • Ensure you’re using the exact secret from your webhook settings
  • Verify you’re signing the raw request body (not parsed JSON)
  • Check that your HMAC implementation matches Dub’s algorithm

High Failure Rate

  • Ensure your endpoint responds within 30 seconds
  • Check for errors in your webhook handler code
  • Verify your server isn’t being rate-limited or blocked

Next Steps

API Reference

View the full API documentation

Analytics Integrations

Send events to analytics platforms

Conversion Tracking

Learn about tracking leads and sales

Affiliate Programs

Set up affiliate and referral programs

Build docs developers (and LLMs) love