Skip to content

Conversation

@KMKoushik
Copy link
Member

@KMKoushik KMKoushik commented Jan 4, 2026


Summary by cubic

Adds first-class webhooks across the app with signed delivery, retries, logging, and a full dashboard UI. Also adds SDK support for signature verification with typed events.

  • New Features

    • Database: Webhook and WebhookCall models with statuses, metrics, and a 30‑day cleanup job.
    • Delivery: HMAC‑SHA256 signatures with timestamp, exponential backoff, auto‑disable on failures, pause/resume, test send, and per‑call retry.
    • UI: Webhooks list, create/edit/delete, detail view with recent calls, payloads, filters, status badges, and retry.
    • API: tRPC router for CRUD, list calls with pagination/filtering, retry, test, and status changes.
    • Events: Email (SES), Contact, and Domain changes now emit typed webhook events.
    • SDK: New Webhooks class with constructEvent and header constants for signature verification and strong typing; README updated.
    • Shared lib: New @usesend/lib with webhook event types and SES error constants; updated imports.
    • Limits: Plan quotas for webhooks with server‑side checks and limits API support.
  • Migration

    • pnpm install
    • pnpm prisma migrate deploy
    • Ensure Redis worker is running (new queues: webhook-dispatch, webhook-cleanup).
    • For local dev, pull the updated SES/SNS image: usesend/local-ses-sns:latest.

Written for commit 20a74de. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Full Webhooks UI: create/edit/pause/resume/rotate/delete, detailed webhook view, call history, payload viewer, retry & test.
  • Updates

    • Automatic webhook emission for contact, domain, and email events; delivery queue with retries, backoff, auto-disable; background cleanup of old calls; per-plan webhook limits; Webhooks added to Settings navigation.
  • Documentation

    • Comprehensive webhook architecture guide and SDK/Webhooks usage docs.
  • Chores

    • Added shared webhook library and SDK webhook verification/handling support.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
unsend-marketing Ready Ready Preview, Comment Jan 18, 2026 9:41am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 4, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds full webhook support: DB migration and Prisma models for Webhook and WebhookCall (enums, indexes, FKs); server-side WebhookService and WebhookQueueService with delivery, signing, retries, backoff, locking, and auto-disable; TRPC router endpoints for webhook CRUD, calls, test, and retry; emits webhook events from contacts, domains, and SES parsing; dashboard UI (list, detail, create/edit/delete, calls table/details); SDK and packages/lib webhook types/utilities; plan-limit checks, cleanup job, and queue constants.

Possibly related PRs

Suggested labels

codex

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add webhooks' directly and accurately describes the primary change in the changeset: comprehensive webhook functionality including database models, delivery system, UI, API, events, and SDK utilities.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 55 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx:220">
P2: Missing error state handling: The component doesn&#39;t check `webhookQuery.isError`. If the API call fails, users will see &quot;Webhook not found&quot; instead of an appropriate error message, which could be misleading.</violation>
</file>

<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx:118">
P2: Using `call.responseTimeMs &amp;&amp;` will hide the duration when it&#39;s 0ms, which is a valid response time. Use a null check instead.</violation>
</file>

<file name="packages/sdk/package.json">

<violation number="1" location="packages/sdk/package.json:28">
P0: Adding a private workspace package as a runtime dependency to a published SDK will break npm installations. `@usesend/lib` is marked `private: true` and won&#39;t be published to npm, causing dependency resolution failures for SDK users.

Consider either:
1. Moving `@usesend/lib` to `devDependencies` and bundling its code at build time (add `--noExternal @usesend/lib` to the tsup command)
2. Making `@usesend/lib` a public package that&#39;s also published to npm</violation>
</file>

<file name="apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx:90">
P2: Incorrect `colSpan` value. The table has 5 columns but `colSpan={6}` is used. This will cause layout inconsistency.</violation>
</file>

<file name="packages/sdk/README.md">

<violation number="1" location="packages/sdk/README.md:131">
P2: The Next.js example is missing error handling for `constructEvent()`. Unlike the Express example below which shows proper try/catch, this code will throw an unhandled error when signature verification fails, resulting in a 500 instead of a 400 response. Consider wrapping in try/catch for consistency:

```ts
export async function POST(request: Request) {
  try {
    const rawBody = await request.text();
    const event = constructEvent({
      secret: process.env.USESEND_WEBHOOK_SECRET!,
      headers: request.headers,
      rawBody,
    });
    // handle event...
    return new Response(&quot;ok&quot;);
  } catch (error) {
    return new Response((error as Error).message, { status: 400 });
  }
}
```</violation>
</file>

<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx:43">
P2: `navigator.clipboard.writeText` returns a Promise that can reject. Add error handling to avoid showing success toast when copy fails.</violation>
</file>

<file name="apps/web/src/components/code-display.tsx">

<violation number="1" location="apps/web/src/components/code-display.tsx:63">
P2: The `setTimeout` in `handleCopy` is not cleaned up on unmount, which can cause a React state update on an unmounted component. Consider using a ref to track the timeout and clear it in a cleanup function, or use a custom hook for this pattern.</violation>
</file>

<file name="apps/web/src/server/service/webhook-service.ts">

<violation number="1" location="apps/web/src/server/service/webhook-service.ts:783">
P2: Stream reader not canceled when truncating. When breaking early due to exceeding the response text limit, call `reader.cancel()` to properly release the underlying connection resources.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (20)
packages/lib/package.json (1)

10-12: Consider adding a build script if distributable artifacts are needed.

The package has no build script despite the tsconfig.json being configured to output to "dist". If this package is meant to be consumed as built artifacts rather than source files, you should add a build script.

Proposed build script addition
  "scripts": {
+   "build": "tsc",
    "lint": "eslint . --max-warnings 0",
    "lint:fix": "eslint . --fix"
  },
packages/sdk/README.md (1)

129-142: Suggest adding error handling to the Next.js example.

The Next.js App Route example currently lacks error handling, while the Express example (lines 169-183) includes a try-catch block. For webhook verification, explicit error handling is recommended because:

  1. Verification failures should return 400 (Bad Request) rather than 500 (Internal Server Error)
  2. Webhook senders use HTTP status codes to determine retry behavior
  3. Consistency between framework examples aids developer understanding

Consider adding a try-catch wrapper similar to the Express example to demonstrate proper error handling and return appropriate HTTP status codes for verification failures.

Suggested enhancement
 // In a Next.js App Route
 export async function POST(request: Request) {
-  const rawBody = await request.text(); // important: raw body, not parsed JSON
-  const event = constructEvent({
-    secret: process.env.USESEND_WEBHOOK_SECRET!,
-    headers: request.headers,
-    rawBody,
-  });
-
-  if (event.type === "email.delivered") {
-    // event.data is strongly typed here
+  try {
+    const rawBody = await request.text();
+    const event = constructEvent({
+      secret: process.env.USESEND_WEBHOOK_SECRET!,
+      headers: request.headers,
+      rawBody,
+    });
+
+    if (event.type === "email.delivered") {
+      // event.data is strongly typed here
+    }
+
+    return new Response("ok");
+  } catch (error) {
+    return new Response((error as Error).message, { status: 400 });
   }
-
-  return new Response("ok");
 }
apps/web/src/components/code-display.tsx (1)

44-49: Consider displaying raw code as fallback on highlighting failure.

When codeToHtml fails, html remains empty and isLoading becomes false, resulting in an empty container. Showing the raw code as a fallback would provide a better user experience.

🔎 Proposed fix
       } catch (error) {
         console.error("Failed to highlight code:", error);
         if (isMounted) {
+          setHtml(""); // Ensure html is empty to trigger fallback
           setIsLoading(false);
         }
       }

Then in the render section (around line 94):

+  if (!html) {
+    // Fallback: show raw code without highlighting
+    return (
+      <div className="relative rounded-lg overflow-hidden border bg-muted/50">
+        <Button
+          size="icon"
+          variant="ghost"
+          onClick={handleCopy}
+          className="absolute top-2 right-2 h-8 w-8 z-10"
+        >
+          {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
+        </Button>
+        <pre
+          className={`text-xs font-mono p-4 overflow-auto ${className}`}
+          style={{ maxHeight }}
+        >
+          <code className="p-2">{code}</code>
+        </pre>
+      </div>
+    );
+  }
+
   return (
apps/web/src/app/(dashboard)/webhooks/page.tsx (1)

4-5: Consider using the ~/ alias for consistency.

Per coding guidelines, imports in apps/web should use the ~/ alias instead of relative paths.

🔎 Suggested refactor
-import { AddWebhook } from "./add-webhook";
-import { WebhookList } from "./webhook-list";
+import { AddWebhook } from "~/app/(dashboard)/webhooks/add-webhook";
+import { WebhookList } from "~/app/(dashboard)/webhooks/webhook-list";

Based on coding guidelines: "Use alias ~/ for src imports in apps/web"

apps/web/src/server/jobs/webhook-cleanup-job.ts (1)

14-34: Consider batching large deletions for better database performance.

The deleteMany operation at line 18 could potentially delete millions of records in a single transaction if webhook calls have accumulated over time. This might cause:

  • Long-running transactions that lock the webhookCall table
  • Memory pressure from processing large result sets
  • Potential timeouts or connection issues
🔎 Suggested batched deletion approach
 const worker = new Worker(
   WEBHOOK_CLEANUP_QUEUE,
   async () => {
     const cutoff = subDays(new Date(), WEBHOOK_RETENTION_DAYS);
-    const result = await db.webhookCall.deleteMany({
-      where: {
-        createdAt: {
-          lt: cutoff,
-        },
-      },
-    });
+    
+    const BATCH_SIZE = 1000;
+    let totalDeleted = 0;
+    let hasMore = true;
+
+    while (hasMore) {
+      const result = await db.webhookCall.deleteMany({
+        where: {
+          id: {
+            in: (
+              await db.webhookCall.findMany({
+                where: { createdAt: { lt: cutoff } },
+                select: { id: true },
+                take: BATCH_SIZE,
+              })
+            ).map((r) => r.id),
+          },
+        },
+      });
+
+      totalDeleted += result.count;
+      hasMore = result.count === BATCH_SIZE;
+    }

     logger.info(
-      { deleted: result.count, cutoff: cutoff.toISOString() },
+      { deleted: totalDeleted, cutoff: cutoff.toISOString() },
       "[WebhookCleanupJob]: Deleted old webhook calls",
     );
   },
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (2)

25-40: Optimize the filtering and counting logic.

The current implementation:

  1. Filters calls to the last 7 days (line 26-28)
  2. Iterates three separate times to count different statuses (lines 30-40)

This can be optimized by combining the filtering and counting into a single pass, and by creating the sevenDaysAgo date object once instead of creating a new Date object for each call during filtering.

🔎 Proposed optimization
  const calls = callsQuery.data?.items ?? [];
- const last7DaysCalls = calls.filter(
-   (call) => new Date(call.createdAt) >= sevenDaysAgo,
- );
-
- const deliveredCount = last7DaysCalls.filter(
-   (c) => c.status === WebhookCallStatus.DELIVERED,
- ).length;
- const failedCount = last7DaysCalls.filter(
-   (c) => c.status === WebhookCallStatus.FAILED,
- ).length;
- const pendingCount = last7DaysCalls.filter(
-   (c) =>
-     c.status === WebhookCallStatus.PENDING ||
-     c.status === WebhookCallStatus.IN_PROGRESS,
- ).length;
+ 
+ const sevenDaysAgoTime = sevenDaysAgo.getTime();
+ let deliveredCount = 0;
+ let failedCount = 0;
+ let pendingCount = 0;
+ let last7DaysCalls = [];
+ 
+ for (const call of calls) {
+   if (new Date(call.createdAt).getTime() >= sevenDaysAgoTime) {
+     last7DaysCalls.push(call);
+     if (call.status === WebhookCallStatus.DELIVERED) {
+       deliveredCount++;
+     } else if (call.status === WebhookCallStatus.FAILED) {
+       failedCount++;
+     } else if (
+       call.status === WebhookCallStatus.PENDING ||
+       call.status === WebhookCallStatus.IN_PROGRESS
+     ) {
+       pendingCount++;
+     }
+   }
+ }

20-23: Add server-side date filtering to the listCalls TRPC endpoint.

The component fetches 50 webhook calls and filters them client-side to the last 7 days. The api.webhook.listCalls endpoint does not currently support date range parameters. Adding createdAfter and createdBefore parameters to the TRPC input schema and WebhookService.listWebhookCalls method would allow filtering on the server side, reducing unnecessary data transfer and improving performance.

apps/web/src/server/service/ses-hook-parser.ts (1)

397-428: Make the default case more explicit.

The default case returns "email.queued" for any unhandled status, including SCHEDULED. While SCHEDULED emails likely don't reach this code path via SES hooks, the implicit fallback could mask future issues when new statuses are added.

🔎 Proposed fix to handle SCHEDULED explicitly
 function emailStatusToEvent(status: EmailStatus): EmailWebhookEventType {
   switch (status) {
     case EmailStatus.QUEUED:
       return "email.queued";
     case EmailStatus.SENT:
       return "email.sent";
     case EmailStatus.DELIVERY_DELAYED:
       return "email.delivery_delayed";
     case EmailStatus.DELIVERED:
       return "email.delivered";
     case EmailStatus.BOUNCED:
       return "email.bounced";
     case EmailStatus.REJECTED:
       return "email.rejected";
     case EmailStatus.RENDERING_FAILURE:
       return "email.rendering_failure";
     case EmailStatus.COMPLAINED:
       return "email.complained";
     case EmailStatus.FAILED:
       return "email.failed";
     case EmailStatus.CANCELLED:
       return "email.cancelled";
     case EmailStatus.SUPPRESSED:
       return "email.suppressed";
     case EmailStatus.OPENED:
       return "email.opened";
     case EmailStatus.CLICKED:
       return "email.clicked";
+    case EmailStatus.SCHEDULED:
+      // SCHEDULED emails should not trigger webhooks via SES
+      logger.warn({ status }, "Unexpected SCHEDULED status in SES hook");
+      return "email.queued";
     default:
-      return "email.queued";
+      logger.error({ status }, "Unknown email status in emailStatusToEvent");
+      return "email.queued";
   }
 }
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

144-152: Consider rendering EditWebhookDialog unconditionally.

The current pattern conditionally mounts/unmounts the EditWebhookDialog component for each webhook. This causes the component to reinitialize on every open, which is less efficient. Since the dialog is already controlled via the open prop, you could render it once outside the map loop.

🔎 Proposed refactor to improve efficiency

Move the dialog outside the map loop and track the webhook being edited:

 export function WebhookList() {
   const webhooksQuery = api.webhook.list.useQuery();
   const setStatusMutation = api.webhook.setStatus.useMutation();
   const utils = api.useUtils();
   const router = useRouter();
-  const [editingId, setEditingId] = useState<string | null>(null);
+  const [editingWebhook, setEditingWebhook] = useState<Webhook | null>(null);

   const webhooks = webhooksQuery.data ?? [];

   // ... handlers ...

   return (
     <div className="mt-10">
       <div className="rounded-xl border shadow">
         <Table>
           {/* ... table header ... */}
           <TableBody>
             {/* ... loading/empty states ... */}
             {webhooks.map((webhook) => (
               <TableRow
                 key={webhook.id}
                 className="cursor-pointer hover:bg-muted/50"
                 onClick={() => router.push(`/webhooks/${webhook.id}`)}
               >
                 {/* ... table cells ... */}
                 <TableCell className="text-right">
                   <div
                     className="flex items-center justify-end"
                     onClick={(e) => e.stopPropagation()}
                   >
                     <WebhookActions
                       webhook={webhook}
-                      onEdit={() => setEditingId(webhook.id)}
+                      onEdit={() => setEditingWebhook(webhook)}
                       onToggleStatus={() =>
                         handleToggleStatus(webhook.id, webhook.status)
                       }
                       isToggling={setStatusMutation.isPending}
                     />
                   </div>
-                  {editingId === webhook.id ? (
-                    <EditWebhookDialog
-                      webhook={webhook}
-                      open={editingId === webhook.id}
-                      onOpenChange={(open) =>
-                        setEditingId(open ? webhook.id : null)
-                      }
-                    />
-                  ) : null}
                 </TableCell>
               </TableRow>
             ))}
           </TableBody>
         </Table>
       </div>
+      {editingWebhook && (
+        <EditWebhookDialog
+          webhook={editingWebhook}
+          open={!!editingWebhook}
+          onOpenChange={(open) => setEditingWebhook(open ? editingWebhook : null)}
+        />
+      )}
     </div>
   );
 }
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (2)

145-160: Unused formState variable in URL field render.

The formState is destructured but not used in this field's render function. Consider removing it to clean up the code.

Proposed fix
             <FormField
               control={form.control}
               name="url"
-              render={({ field, formState }) => (
+              render={({ field }) => (
                 <FormItem>
                   <FormLabel>Endpoint URL</FormLabel>

181-240: Consider extracting shared event selection logic.

The handleToggleAll, handleToggleGroup, handleToggleEvent, and isGroupFullySelected functions are nearly identical to those in add-webhook.tsx. Consider extracting these into a shared custom hook (e.g., useWebhookEventSelection) to reduce duplication.

apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)

151-166: Unused formState variable in URL field render.

Similar to the edit dialog, formState is destructured but not used in this field's render function.

Proposed fix
             <FormField
               control={form.control}
               name="url"
-              render={({ field, formState }) => (
+              render={({ field }) => (
                 <FormItem>
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)

22-23: Remove unused import.

CircleEllipsis is imported but not used in the component.

Proposed fix
   Edit3,
   Key,
   MoreVertical,
   Pause,
   Play,
   TestTube,
-  CircleEllipsis,
 } from "lucide-react";

179-195: Use WebhookStatus enum for type safety.

The status comparison and mutation use string literals ("ACTIVE", "PAUSED") instead of the WebhookStatus enum. This could cause runtime issues if enum values change.

Proposed fix
+import { WebhookStatus } from "@prisma/client";

 const handleToggleStatus = (currentStatus: string) => {
-    const newStatus = currentStatus === "ACTIVE" ? "PAUSED" : "ACTIVE";
+    const newStatus = currentStatus === WebhookStatus.ACTIVE 
+      ? WebhookStatus.PAUSED 
+      : WebhookStatus.ACTIVE;
     setStatusMutation.mutate(
       { id: webhookId, status: newStatus },

Also update the function signature to use the enum type:

-  const handleToggleStatus = (currentStatus: string) => {
+  const handleToggleStatus = (currentStatus: WebhookStatus) => {
packages/sdk/src/webhooks.ts (2)

2-7: Consider using a proper package subpath export instead of importing from src/.

The import path @usesend/lib/src/webhook/webhook-events reaches into the package's internal src/ directory. This pattern can be fragile if the package structure changes and doesn't follow conventional package subpath exports.

Consider configuring a proper subpath export in @usesend/lib's package.json (e.g., @usesend/lib/webhook) to make this import more stable and explicit as part of the public API.


180-187: Document the negative tolerance behavior.

The condition toleranceMs >= 0 means passing a negative tolerance value disables the timestamp check entirely. While this may be intentional for testing scenarios, it's not documented in the JSDoc comments and could be misused.

Consider either documenting this behavior in the options type or removing the ability to disable tolerance checks via negative values.

apps/web/src/server/service/webhook-service.ts (3)

43-83: Consider lazy initialization for Queue and Worker.

The static initialization creates Redis connections immediately when the module is imported. This could cause issues in serverless/edge environments or during build time if the Redis connection isn't available.

Consider using a lazy initialization pattern (e.g., singleton getter) to defer connection until the first actual use.


235-240: Consider providing a user-friendly error message.

The error message uses reason ?? "Webhook limit reached" where reason is a LimitReason enum value. Depending on how LimitReason is defined, the message might display an internal enum name (e.g., "WEBHOOK") rather than a user-friendly message.

Consider mapping the LimitReason to a descriptive message string.


766-768: Consider extracting the skip threshold to a named constant.

The WEBHOOK_RESPONSE_TEXT_LIMIT * 2 is a magic number for determining when to skip reading the response entirely. Consider extracting this to a named constant for clarity (e.g., WEBHOOK_RESPONSE_SKIP_THRESHOLD).

packages/lib/src/webhook/webhook-events.ts (1)

151-165: Consider adding extended payload for email.complained.

Looking at the payload map, email.complained uses EmailBasePayload without complaint-specific details. If the complaint data (e.g., feedback type, complaint reason) is available from the email provider, consider adding an EmailComplainedPayload type similar to EmailBouncedPayload.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bba9e93 and 62d2baa.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (54)
  • apps/web/package.json
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/server/api/routers/email.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/contact-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/tailwind.config.ts
  • docker/dev/compose.yml
  • packages/lib/.eslintrc.cjs
  • packages/lib/index.ts
  • packages/lib/package.json
  • packages/lib/src/constants/ses-errors.ts
  • packages/lib/src/index.ts
  • packages/lib/src/webhook/webhook-events.ts
  • packages/lib/tsconfig.json
  • packages/lib/tsconfig.lint.json
  • packages/sdk/README.md
  • packages/sdk/index.ts
  • packages/sdk/package.json
  • packages/sdk/src/usesend.ts
  • packages/sdk/src/webhooks.ts
  • packages/ui/src/dropdown-menu.tsx
  • packages/ui/styles/globals.css
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • packages/sdk/src/usesend.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • packages/sdk/index.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • packages/lib/src/index.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • packages/sdk/src/webhooks.ts
  • packages/lib/src/webhook/webhook-events.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/contact-service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • packages/sdk/src/usesend.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • packages/sdk/index.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • packages/lib/src/index.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • packages/sdk/src/webhooks.ts
  • packages/lib/src/webhook/webhook-events.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/contact-service.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/server/service/contact-service.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/server/service/contact-service.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • packages/sdk/src/usesend.ts
  • packages/sdk/README.md
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • packages/sdk/index.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • packages/lib/src/index.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • packages/sdk/src/webhooks.ts
  • packages/lib/src/webhook/webhook-events.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/contact-service.ts
🧠 Learnings (7)
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run ESLint via usesend/eslint-config and ensure no warnings remain before submitting PRs

Applied to files:

  • packages/lib/package.json
  • packages/lib/.eslintrc.cjs
  • packages/lib/tsconfig.json
  • apps/web/tailwind.config.ts
  • packages/lib/tsconfig.lint.json
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages

Applied to files:

  • packages/lib/package.json
  • packages/lib/.eslintrc.cjs
  • packages/lib/tsconfig.json
  • apps/web/tailwind.config.ts
  • packages/lib/tsconfig.lint.json
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code

Applied to files:

  • apps/web/src/components/code-display.tsx
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web

Applied to files:

  • apps/web/src/server/api/routers/webhook.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : Use alias `~/` for src imports in apps/web (e.g., `import { x } from "~/utils/x"`)

Applied to files:

  • apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,md} : Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Applied to files:

  • apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Use Prisma for database access in apps/web

Applied to files:

  • apps/web/prisma/schema.prisma
🧬 Code graph analysis (21)
apps/web/src/app/(dashboard)/webhooks/page.tsx (2)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)
  • AddWebhook (66-333)
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
  • WebhookList (29-162)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (4)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/badge.tsx (1)
  • Badge (36-36)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
  • WebhookStatusBadge (1-23)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
apps/web/src/components/DeleteResource.tsx (1)
  • DeleteResource (63-199)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (5)
packages/ui/src/select.tsx (5)
  • Select (150-150)
  • SelectTrigger (153-153)
  • SelectValue (152-152)
  • SelectContent (154-154)
  • SelectItem (156-156)
packages/ui/src/table.tsx (5)
  • Table (109-109)
  • TableRow (114-114)
  • TableHead (113-113)
  • TableBody (111-111)
  • TableCell (115-115)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
  • WebhookCallStatusBadge (3-41)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/server/api/routers/contacts.ts (1)
apps/web/src/server/api/trpc.ts (1)
  • contactBookProcedure (204-222)
apps/web/src/server/public-api/api/contacts/delete-contact.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • deleteContact (97-107)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (4)
packages/lib/src/webhook/webhook-events.ts (5)
  • WebhookEvents (36-40)
  • WebhookEventType (42-42)
  • ContactEvents (1-5)
  • DomainEvents (9-14)
  • EmailEvents (18-32)
packages/sdk/index.ts (1)
  • WebhookEventType (16-16)
packages/sdk/src/webhooks.ts (1)
  • WebhookEventType (278-278)
apps/web/src/store/upgradeModalStore.ts (1)
  • useUpgradeModalStore (13-20)
packages/sdk/src/usesend.ts (2)
packages/sdk/index.ts (1)
  • Webhooks (5-5)
packages/sdk/src/webhooks.ts (1)
  • Webhooks (45-199)
apps/web/src/server/public-api/api/contacts/update-contact.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • updateContact (80-95)
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (3)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
  • WebhookStatusBadge (1-23)
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)
  • EditWebhookDialog (66-323)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
  • DeleteWebhook (11-61)
apps/web/src/server/jobs/webhook-cleanup-job.ts (4)
apps/web/src/server/queue/queue-constants.ts (2)
  • WEBHOOK_CLEANUP_QUEUE (7-7)
  • DEFAULT_QUEUE_OPTIONS (9-14)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/api/routers/webhook.ts (2)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • teamProcedure (127-152)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/webhook.ts (1)
  • webhookRouter (9-135)
apps/web/src/server/service/contact-queue-service.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • addOrUpdateContact (19-78)
apps/web/src/server/service/domain-service.ts (2)
packages/lib/src/webhook/webhook-events.ts (2)
  • DomainPayload (86-100)
  • DomainWebhookEventType (16-16)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
apps/web/src/server/api/routers/limits.ts (1)
apps/web/src/server/service/limit-service.ts (1)
  • LimitService (19-280)
apps/web/src/server/service/ses-hook-parser.ts (3)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
packages/lib/src/webhook/webhook-events.ts (3)
  • EmailEventPayloadMap (151-165)
  • EmailStatus (44-58)
  • EmailBasePayload (60-72)
apps/web/src/types/aws-types.ts (4)
  • SesEvent (143-156)
  • SesEventDataKey (132-141)
  • SesBounce (30-50)
  • SesClick (91-97)
packages/sdk/src/webhooks.ts (2)
packages/sdk/index.ts (7)
  • WebhookVerificationError (6-6)
  • WEBHOOK_SIGNATURE_HEADER (9-9)
  • WEBHOOK_TIMESTAMP_HEADER (10-10)
  • WEBHOOK_EVENT_HEADER (7-7)
  • WEBHOOK_CALL_HEADER (8-8)
  • Webhooks (5-5)
  • WebhookEventData (14-14)
packages/lib/src/webhook/webhook-events.ts (1)
  • WebhookEventData (194-196)
apps/web/src/server/service/webhook-service.ts (6)
apps/web/src/server/queue/bullmq-context.ts (2)
  • TeamJob (5-5)
  • createWorkerHandler (10-24)
packages/lib/src/webhook/webhook-events.ts (2)
  • WebhookEventType (42-42)
  • WebhookPayloadData (184-185)
apps/web/src/server/queue/queue-constants.ts (2)
  • WEBHOOK_DISPATCH_QUEUE (6-6)
  • DEFAULT_QUEUE_OPTIONS (9-14)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/public-api/api-error.ts (1)
  • UnsendApiError (62-75)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx (1)
packages/ui/src/code-block.tsx (1)
  • CodeBlock (13-36)
apps/web/src/server/service/contact-service.ts (4)
apps/web/src/server/db.ts (1)
  • db (20-20)
packages/lib/src/webhook/webhook-events.ts (2)
  • ContactWebhookEventType (7-7)
  • ContactPayload (74-84)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
🪛 ast-grep (0.40.3)
apps/web/src/components/code-display.tsx

[warning] 106-106: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/code-display.tsx

[error] 107-107: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Cloudflare Pages

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 4, 2026

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 20a74de
Status:⚡️  Build in progress...

View logs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)

113-113: LGTM! The colSpan fix has been applied.

The previous issue with colSpan={5} has been corrected to colSpan={3}, which now matches the actual number of columns in the table.

Also applies to: 122-122

apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

31-31: Remove unused mutation import.

The testWebhook mutation is imported but never used in the component. This appears to be leftover from when the handleTest function was removed.

🔎 Proposed fix
 export function WebhookList() {
   const webhooksQuery = api.webhook.list.useQuery();
-  const testWebhook = api.webhook.test.useMutation();
   const setStatusMutation = api.webhook.setStatus.useMutation();
🧹 Nitpick comments (3)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (2)

99-157: Consider using a single table for better column alignment and accessibility.

The current implementation splits the table into two separate <Table> components—one for the header and one for the body—to achieve a sticky header with scrollable content. While this works functionally, it can lead to:

  1. Column width misalignment between header and body (not guaranteed to stay in sync)
  2. Semantic HTML issues (header and body should be in the same table element)
  3. Accessibility concerns for screen readers expecting a unified table structure
🔎 Recommended approach using a single table

Consider using a single <Table> with CSS to make the header sticky:

-      <div className="flex-1 overflow-hidden rounded-xl border shadow flex flex-col">
-        <Table>
-          <TableHeader className="sticky top-0 z-10">
-            <TableRow className="bg-muted dark:bg-muted/70">
-              <TableHead className="h-9 rounded-tl-xl">Status</TableHead>
-              <TableHead className="h-9">Event Type</TableHead>
-              <TableHead className="h-9 rounded-tr-xl">Time</TableHead>
-            </TableRow>
-          </TableHeader>
-        </Table>
-        <div className="flex-1 overflow-auto no-scrollbar">
-          <Table>
-            <TableBody>
+      <div className="flex-1 overflow-auto no-scrollbar rounded-xl border shadow">
+        <Table>
+          <TableHeader className="sticky top-0 z-10 bg-muted dark:bg-muted/70">
+            <TableRow>
+              <TableHead className="h-9">Status</TableHead>
+              <TableHead className="h-9">Event Type</TableHead>
+              <TableHead className="h-9">Time</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
               {/* ... table body content ... */}
-            </TableBody>
-          </Table>
-        </div>
+          </TableBody>
+        </Table>
       </div>

This approach maintains column alignment and semantic correctness. If the rounded corners on the header are important, they can be achieved with additional CSS targeting the first/last TableHead elements.


111-119: Consider adding ARIA labels to the loading spinner for screen reader users.

While the spinner is visually clear, screen reader users would benefit from an explicit loading announcement.

🔎 Suggested enhancement
                   <TableCell colSpan={3} className="py-4 text-center">
+                    <div role="status" aria-live="polite" aria-label="Loading webhook calls">
                       <Spinner
                         className="mx-auto h-6 w-6"
                         innerSvgClass="stroke-primary"
                       />
+                    </div>
                   </TableCell>

Alternatively, add aria-busy="true" to the table wrapper during loading and include visually hidden text.

references/webhook-architecture.md (1)

7-129: Consider adding language identifiers to fenced code blocks.

The architecture diagram and state machine use fenced code blocks without language identifiers. While markdownlint flags this, these ASCII art diagrams don't have appropriate language tags. You could use text or ascii to satisfy the linter while maintaining readability.

🔎 Example fix for the architecture diagram
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────────────────────┐
 │                                    EVENT SOURCES                                     │
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62d2baa and 044e408.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/service/webhook-service.ts
  • packages/sdk/README.md
  • packages/sdk/package.json
  • packages/sdk/src/usesend.ts
  • references/webhook-architecture.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • packages/sdk/package.json
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • packages/sdk/README.md
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
  • references/webhook-architecture.md
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
🧬 Code graph analysis (1)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (2)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
  • WebhookCallStatusBadge (3-41)
🪛 markdownlint-cli2 (0.18.1)
references/webhook-architecture.md

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


133-133: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


173-173: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


320-320: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


364-364: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
packages/sdk/README.md (1)

118-193: LGTM! Webhook documentation is comprehensive and accurate.

The webhook verification examples now include proper error handling in both Next.js and Express, headers are well-documented, and the typed event usage is clearly demonstrated.

apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

39-212: LGTM! Component logic is well-structured.

The status toggle handler, table rendering with correct colSpan={5}, row navigation, and actions menu are all implemented correctly. Good use of event propagation control to prevent row clicks when interacting with the actions popover.

apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql (1)

1-66: LGTM! Database migration is well-structured.

The migration correctly:

  • Defines comprehensive status enums for webhooks and calls
  • Creates properly indexed tables with appropriate foreign key constraints
  • Uses CASCADE for cleanup and SET NULL for optional references
  • Includes composite indexes optimized for common query patterns (teamId + webhookId + status)
apps/web/src/server/service/webhook-service.ts (6)

42-82: LGTM! Queue service initialization is clean.

The static initialization pattern for BullMQ queue and worker is appropriate, with proper error handling and observability via logging. Configuration constants are well-defined at the module level.


84-133: LGTM! Event emission logic is robust.

The emit method correctly:

  • Filters for ACTIVE webhooks with matching event subscriptions (including "all events" via empty array)
  • Persists WebhookCall records before enqueueing for reliability
  • Handles payload serialization safely

135-390: LGTM! Service methods follow solid patterns.

The retry, test, and CRUD methods demonstrate:

  • Consistent error handling with appropriate UnsendApiError codes
  • Proper team-based scoping for multi-tenancy security
  • Limit enforcement on webhook creation
  • Safe status transitions (resetting failure counters on activation)

409-574: LGTM! Webhook delivery worker is well-implemented.

The processWebhookCall function handles the complete delivery lifecycle correctly:

  • Discards calls for inactive webhooks
  • Acquires per-webhook locks to ensure ordered delivery
  • Handles both success and failure paths with proper status updates
  • Implements exponential backoff with attempt limits
  • Auto-disables webhooks after consecutive failure threshold
  • Captures response metadata for observability

393-407: LGTM! Helper functions are correctly implemented.

The utility functions demonstrate best practices:

  • stringifyPayload with safe error handling and fallback
  • acquireLock using Redis SET NX PX pattern
  • releaseLock with Lua script for atomic check-and-delete
  • computeBackoff with 30% jitter to prevent thundering herd
  • buildPayload with safe JSON parsing

Also applies to: 576-648


650-821: LGTM! HTTP delivery and signing implementation is robust.

The webhook HTTP POST logic is well-implemented:

  • AbortController properly handles request timeouts
  • HMAC-SHA256 signature follows standard format (v1=<hex>)
  • Response capture handles both direct text and streaming bodies
  • Stream reader is properly canceled when truncating (line 800) ✓
  • WebhookHttpError captures response metadata for diagnostics

Comment on lines +130 to +137
<TableRow
key={call.id}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add keyboard accessibility to clickable table rows.

The table rows are clickable but lack keyboard accessibility. Users navigating with a keyboard cannot interact with these rows because they're missing tabIndex, onKeyDown, and appropriate ARIA attributes.

🔎 Proposed fix to add keyboard support
                 calls.map((call) => (
                   <TableRow
                     key={call.id}
+                    role="button"
+                    tabIndex={0}
                     className={`cursor-pointer transition-colors ${
                       selectedCallId === call.id
                         ? "bg-accent/50 text-accent-foreground"
                         : "hover:bg-muted/50"
                     }`}
                     onClick={() => onSelectCall(call.id)}
+                    onKeyDown={(e) => {
+                      if (e.key === "Enter" || e.key === " ") {
+                        e.preventDefault();
+                        onSelectCall(call.id);
+                      }
+                    }}
                   >

This enables keyboard users to:

  • Tab to each row
  • Press Enter or Space to select a webhook call
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableRow
key={call.id}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
<TableRow
key={call.id}
role="button"
tabIndex={0}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelectCall(call.id);
}
}}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
around lines 130-137, the clickable TableRow lacks keyboard accessibility; add
tabIndex={0} so rows are focusable, add an onKeyDown handler that invokes
onSelectCall(call.id) when Enter or Space is pressed, and include appropriate
ARIA attributes (e.g., role="button" and aria-selected={selectedCallId ===
call.id}) so assistive tech knows the row is interactive and its selection
state; keep the existing onClick behavior intact.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
packages/lib/tsconfig.lint.json (1)

6-6: Remove the non-existent "turbo" directory from the include array.

As flagged in a previous review, the "turbo" directory does not exist in packages/lib and should be removed from the include array.

🔎 Proposed fix
-  "include": ["src", "turbo", "**/*.ts", "**/*.tsx"],
+  "include": ["src", "**/*.ts", "**/*.tsx"],
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (1)

41-44: Add error handling for clipboard operation.

navigator.clipboard.writeText returns a Promise that can reject (e.g., due to permissions). The success toast will show even if the copy fails.

🔎 Proposed fix
-  const handleCopySecret = () => {
-    navigator.clipboard.writeText(webhook.secret);
-    toast.success("Secret copied to clipboard");
+  const handleCopySecret = async () => {
+    try {
+      await navigator.clipboard.writeText(webhook.secret);
+      toast.success("Secret copied to clipboard");
+    } catch {
+      toast.error("Failed to copy secret");
+    }
   };
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)

130-152: Add keyboard accessibility to clickable table rows.

The table rows are interactive via click but cannot be accessed via keyboard navigation. This is an accessibility concern for users who navigate with keyboards.

🔎 Proposed fix
                   <TableRow
                     key={call.id}
+                    role="button"
+                    tabIndex={0}
                     className={`cursor-pointer transition-colors ${
                       selectedCallId === call.id
                         ? "bg-accent/50 text-accent-foreground"
                         : "hover:bg-muted/50"
                     }`}
                     onClick={() => onSelectCall(call.id)}
+                    onKeyDown={(e) => {
+                      if (e.key === "Enter" || e.key === " ") {
+                        e.preventDefault();
+                        onSelectCall(call.id);
+                      }
+                    }}
                   >
apps/web/src/server/service/webhook-service.ts (1)

449-450: Remove outdated TODO comment.

This TODO claims the code is a stub, but the full webhook delivery implementation is present (signed HTTP POST, backoff tracking, status updates, auto-disable logic). The comment is misleading.

🔎 Proposed fix
-  // TODO: perform signed HTTP POST with backoff tracking and update status/metrics.
-  // This stub ensures call rows and queue wiring exist before adding delivery logic.
   const lockKey = `webhook:lock:${call.webhookId}`;
🧹 Nitpick comments (12)
references/webhook-architecture.md (1)

7-7: Consider adding language specifiers to fenced code blocks.

Several fenced code blocks lack language specifiers, which can affect rendering and tooling support. Consider adding appropriate tags:

  • Lines 7, 133: Use ```text for ASCII diagrams
  • Line 173: Use ```text for schema definitions
  • Line 320: Use ```http or ```text for HTTP headers
  • Line 364: Use ```text for the status flow

This improves syntax highlighting and markdown linting compliance.

Example fix for the first diagram
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────────────────────┐
 │                                    EVENT SOURCES                                     │

Also applies to: 133-133, 173-173, 320-320, 364-364

apps/web/src/server/service/ses-hook-parser.ts (2)

372-395: Consider deriving valid subtypes from the type definition.

The function correctly normalizes bounce subtypes, but the validSubTypes array is hardcoded and could drift from the EmailBounceSubType definition over time.

💡 Alternative approach using type-level validation

If the webhook events library exports the valid subtypes as a constant array, you could import and reuse it here to maintain a single source of truth. However, if the type is only available at compile-time, the current approach is acceptable with proper documentation.


430-489: Add explicit return type annotation for type safety.

The function currently has an implicit any return type. Consider adding an explicit return type annotation for better type safety and documentation.

🔎 Suggested improvement
 function buildEmailMetadata(
   status: EmailStatus,
   mailData: SesEvent | SesEvent[SesEventDataKey],
-) {
+): Record<string, unknown> | undefined {

This makes the return type explicit and prevents accidental any type propagation.

apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (2)

154-154: Remove unused formState parameter.

The formState parameter is destructured but never used in this render function.

🔎 Proposed fix
-              render={({ field, formState }) => (
+              render={({ field }) => (

128-133: Simplify onOpenChange handler.

The check nextOpen !== open is unnecessary since onOpenChange is only called when the dialog state actually changes.

🔎 Proposed fix
     <Dialog
       open={open}
-      onOpenChange={(nextOpen) =>
-        nextOpen !== open ? onOpenChange(nextOpen) : null
-      }
+      onOpenChange={onOpenChange}
     >
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (2)

16-16: Remove unused import FormDescription.

FormDescription is imported but never used in this file.

🔎 Proposed fix
 import {
   Form,
   FormControl,
-  FormDescription,
   FormField,
   FormItem,
   FormLabel,
   FormMessage,
 } from "@usesend/ui/src/form";

148-148: Remove unused formState parameter.

The formState parameter is destructured but never used in this render function.

🔎 Proposed fix
-              render={({ field, formState }) => (
+              render={({ field }) => (
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (2)

78-78: Use WebhookCallStatus enum instead of string literal.

For consistency and type safety, import and use the WebhookCallStatus enum rather than the string literal "FAILED".

🔎 Proposed fix
+import { WebhookCallStatus } from "@prisma/client";
+
 // ...
 
-        {call.status === "FAILED" && (
+        {call.status === WebhookCallStatus.FAILED && (

163-170: Remove unnecessary fragment wrapper.

The fragment <>...</> wraps only a single <div> child, making it redundant.

🔎 Proposed fix
         {call.responseText && (
-          <>
-            <div className="flex flex-col gap-3">
-              <h4 className="font-medium text-sm">Response Body</h4>
-              <CodeDisplay code={call.responseText} />
-            </div>
-          </>
+          <div className="flex flex-col gap-3">
+            <h4 className="font-medium text-sm">Response Body</h4>
+            <CodeDisplay code={call.responseText} />
+          </div>
         )}
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)

22-22: Remove unused import CircleEllipsis.

The CircleEllipsis icon is imported but never used in the component.

🔎 Proposed fix
 import {
   Edit3,
   Key,
   MoreVertical,
   Pause,
   Play,
   TestTube,
-  CircleEllipsis,
 } from "lucide-react";

57-58: Consider using WebhookStatus enum for status comparisons.

Lines 57-58 and line 180 use string literals for status comparisons. Using the Prisma-generated enum would provide better type safety.

🔎 Proposed fix
+import { WebhookStatus } from "@prisma/client";

 // In WebhookDetailActions:
-  const isPaused = webhook.status === "PAUSED";
-  const isAutoDisabled = webhook.status === "AUTO_DISABLED";
+  const isPaused = webhook.status === WebhookStatus.PAUSED;
+  const isAutoDisabled = webhook.status === WebhookStatus.AUTO_DISABLED;

 // In handleToggleStatus:
-    const newStatus = currentStatus === "ACTIVE" ? "PAUSED" : "ACTIVE";
+    const newStatus = currentStatus === WebhookStatus.ACTIVE ? WebhookStatus.PAUSED : WebhookStatus.ACTIVE;
packages/sdk/src/webhooks.ts (1)

180-187: Consider documenting the negative tolerance escape hatch.

Setting tolerance to a negative value disables timestamp validation. While this can be useful for testing, it might be worth documenting this behavior in the JSDoc or adding a named constant like SKIP_TIMESTAMP_VALIDATION = -1.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 044e408 and 0116bb8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (55)
  • apps/web/package.json
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/server/api/routers/email.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/contact-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/tailwind.config.ts
  • docker/dev/compose.yml
  • packages/lib/.eslintrc.cjs
  • packages/lib/index.ts
  • packages/lib/package.json
  • packages/lib/src/constants/ses-errors.ts
  • packages/lib/src/index.ts
  • packages/lib/src/webhook/webhook-events.ts
  • packages/lib/tsconfig.json
  • packages/lib/tsconfig.lint.json
  • packages/sdk/README.md
  • packages/sdk/index.ts
  • packages/sdk/package.json
  • packages/sdk/src/usesend.ts
  • packages/sdk/src/webhooks.ts
  • packages/ui/src/dropdown-menu.tsx
  • packages/ui/styles/globals.css
  • references/webhook-architecture.md
🚧 Files skipped from review as they are similar to previous changes (27)
  • packages/sdk/package.json
  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/server/api/routers/email.ts
  • packages/sdk/README.md
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • docker/dev/compose.yml
  • apps/web/tailwind.config.ts
  • packages/lib/tsconfig.json
  • packages/lib/src/index.ts
  • packages/sdk/index.ts
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/lib/package.json
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/service/contact-service.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • packages/ui/styles/globals.css
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/components/AppSideBar.tsx
  • packages/lib/src/webhook/webhook-events.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • packages/sdk/src/webhooks.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • packages/sdk/src/webhooks.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • packages/sdk/src/webhooks.ts
  • apps/web/src/server/service/webhook-service.ts
  • references/webhook-architecture.md
  • apps/web/src/server/api/routers/limits.ts
🧠 Learnings (5)
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code

Applied to files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/components/code-display.tsx
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web

Applied to files:

  • apps/web/src/server/api/routers/webhook.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages

Applied to files:

  • packages/lib/tsconfig.lint.json
  • packages/lib/.eslintrc.cjs
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run ESLint via usesend/eslint-config and ensure no warnings remain before submitting PRs

Applied to files:

  • packages/lib/tsconfig.lint.json
  • packages/lib/.eslintrc.cjs
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Use Prisma for database access in apps/web

Applied to files:

  • apps/web/prisma/schema.prisma
🧬 Code graph analysis (15)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (2)
packages/lib/src/webhook/webhook-events.ts (5)
  • WebhookEvents (36-40)
  • WebhookEventType (42-42)
  • ContactEvents (1-5)
  • DomainEvents (9-14)
  • EmailEvents (18-32)
apps/web/src/store/upgradeModalStore.ts (1)
  • useUpgradeModalStore (13-20)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (3)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
apps/web/src/components/DeleteResource.tsx (1)
  • DeleteResource (63-199)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/webhook.ts (1)
  • webhookRouter (9-135)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (4)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
  • WebhookCallStatusBadge (3-41)
apps/web/src/components/code-display.tsx (1)
  • CodeDisplay (15-111)
apps/web/src/server/service/domain-service.ts (3)
packages/lib/src/webhook/webhook-events.ts (2)
  • DomainPayload (86-100)
  • DomainWebhookEventType (16-16)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (84-391)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (4)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/badge.tsx (1)
  • Badge (36-36)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
  • WebhookStatusBadge (3-25)
packages/ui/src/button.tsx (1)
  • Button (80-80)
packages/sdk/src/usesend.ts (2)
packages/sdk/index.ts (1)
  • Webhooks (5-5)
packages/sdk/src/webhooks.ts (1)
  • Webhooks (45-199)
apps/web/src/server/api/routers/webhook.ts (3)
packages/lib/src/webhook/webhook-events.ts (1)
  • WebhookEvents (36-40)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • teamProcedure (127-152)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (84-391)
apps/web/src/server/service/ses-hook-parser.ts (3)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (84-391)
packages/lib/src/webhook/webhook-events.ts (4)
  • EmailEventPayloadMap (151-165)
  • EmailStatus (44-58)
  • EmailWebhookEventType (34-34)
  • EmailBasePayload (60-72)
apps/web/src/types/aws-types.ts (3)
  • SesEvent (143-156)
  • SesBounce (30-50)
  • SesClick (91-97)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (10)
packages/ui/src/popover.tsx (3)
  • Popover (36-36)
  • PopoverTrigger (36-36)
  • PopoverContent (36-36)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
  • DeleteWebhook (11-61)
apps/web/src/server/service/webhook-service.ts (2)
  • testWebhook (162-194)
  • updateWebhook (256-293)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/breadcrumb.tsx (5)
  • Breadcrumb (108-108)
  • BreadcrumbList (109-109)
  • BreadcrumbItem (110-110)
  • BreadcrumbLink (111-111)
  • BreadcrumbPage (112-112)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (1)
  • WebhookInfo (13-115)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)
  • WebhookCallsTable (28-173)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)
  • WebhookCallDetails (14-174)
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)
  • EditWebhookDialog (66-323)
apps/web/src/components/code-display.tsx (1)
packages/email-editor/src/renderer.tsx (1)
  • code (740-756)
apps/web/src/server/service/contact-queue-service.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • addOrUpdateContact (19-78)
packages/sdk/src/webhooks.ts (2)
packages/sdk/index.ts (7)
  • WebhookVerificationError (6-6)
  • WEBHOOK_SIGNATURE_HEADER (9-9)
  • WEBHOOK_TIMESTAMP_HEADER (10-10)
  • WEBHOOK_EVENT_HEADER (7-7)
  • WEBHOOK_CALL_HEADER (8-8)
  • Webhooks (5-5)
  • WebhookEventData (14-14)
packages/lib/src/webhook/webhook-events.ts (1)
  • WebhookEventData (194-196)
apps/web/src/server/service/webhook-service.ts (7)
apps/web/src/server/queue/bullmq-context.ts (2)
  • TeamJob (5-5)
  • createWorkerHandler (10-24)
packages/lib/src/webhook/webhook-events.ts (2)
  • WebhookEventType (42-42)
  • WebhookPayloadData (184-185)
apps/web/src/server/queue/queue-constants.ts (2)
  • WEBHOOK_DISPATCH_QUEUE (6-6)
  • DEFAULT_QUEUE_OPTIONS (9-14)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/public-api/api-error.ts (1)
  • UnsendApiError (62-75)
apps/web/src/server/service/limit-service.ts (1)
  • LimitService (19-280)
apps/web/src/server/api/routers/limits.ts (1)
apps/web/src/server/service/limit-service.ts (1)
  • LimitService (19-280)
🪛 ast-grep (0.40.3)
apps/web/src/components/code-display.tsx

[warning] 106-106: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/code-display.tsx

[error] 107-107: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 markdownlint-cli2 (0.18.1)
references/webhook-architecture.md

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


133-133: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


173-173: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


320-320: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


364-364: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (32)
apps/web/src/server/service/contact-queue-service.ts (1)

100-108: LGTM! Team-scoped webhook support correctly implemented.

The changes properly extract and forward teamId to enable team-scoped webhook event emission. The implementation is consistent with the existing job data structure and aligns with the updated addOrUpdateContact signature.

packages/lib/.eslintrc.cjs (1)

1-10: LGTM!

The ESLint configuration is correctly structured and follows the monorepo's conventions. It properly extends the shared library config, uses the TypeScript parser, and correctly references the lint-specific tsconfig file.

apps/web/package.json (1)

42-42: LGTM! Dependency addition is correct.

The addition of @usesend/lib as a workspace dependency aligns with the PR's webhook functionality requirements and follows the monorepo pattern used for other internal packages.

apps/web/src/lib/constants/plans.ts (2)

7-7: LGTM! Webhook limit reason added consistently.

The WEBHOOK enum value follows the existing naming convention and integrates properly with the limit checking system.


21-21: LGTM! Webhook limits configured appropriately.

The webhook limits are properly integrated into the plan structure with sensible values: 1 webhook for FREE tier and unlimited (-1) for BASIC tier. This matches the pattern used for other resource limits.

Also applies to: 30-30, 38-38

packages/ui/src/dropdown-menu.tsx (1)

102-113: LGTM! Checkbox item layout improved.

The refactoring moves the checkbox indicator from the left to the right side using ml-auto, which improves the visual hierarchy. The structural changes maintain the same public API while updating the internal rendering order.

apps/web/src/components/code-display.tsx (2)

107-107: Safe usage of dangerouslySetInnerHTML with Shiki.

The dangerouslySetInnerHTML usage here is safe because Shiki's codeToHtml function properly escapes all code content and only generates safe HTML for syntax highlighting. This is an appropriate use case for this API.


15-111: Component implementation looks solid.

The CodeDisplay component properly handles async highlighting with mount tracking, provides a loading state with unstyled code to preserve layout, and includes copy-to-clipboard functionality. The isMounted pattern in the useEffect correctly prevents state updates after unmount.

references/webhook-architecture.md (1)

1-422: Excellent documentation of the webhook architecture.

This documentation provides comprehensive coverage of the webhook system, including clear diagrams, status flows, payload structures, retry logic, and configuration details. The architecture is well-designed with proper reliability features (locking, retries, auto-disable) and the documentation accurately reflects the implementation.

apps/web/src/server/service/ses-hook-parser.ts (3)

1-11: LGTM!

The webhook-related imports are properly structured and necessary for the new webhook emission functionality.

Also applies to: 33-33


279-302: LGTM! Proper error isolation for webhook emission.

The webhook emission is correctly wrapped in a try-catch block to prevent failures from disrupting the main SES hook processing flow. The error logging includes appropriate context.


310-370: LGTM! Well-structured payload builder with type safety.

The function appropriately constructs type-safe webhook payloads for different email statuses with proper fallbacks and optional chaining.

apps/web/src/server/api/routers/limits.ts (1)

21-22: LGTM!

The webhook limit case follows the established pattern and properly delegates to the limit service.

packages/sdk/src/usesend.ts (2)

6-6: LGTM!

The Webhooks import is correctly added to support the new webhook functionality.


175-195: LGTM! The JSDoc example has been corrected.

The webhooks factory method follows the Stripe pattern and is well-documented. The example now correctly shows passing an options object with headers property, addressing the previous review feedback.

apps/web/src/server/api/root.ts (1)

17-17: LGTM!

The webhook router is properly imported and registered, following the established pattern for other routers in the application.

Also applies to: 40-40

apps/web/src/server/queue/queue-constants.ts (1)

6-7: LGTM!

The new queue constants follow the established naming convention and clearly indicate their purpose for webhook dispatch and cleanup operations.

apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)

1-41: LGTM!

The component correctly maps all WebhookCallStatus enum values to appropriate badge colors and labels. The switch statement provides exhaustive coverage with a sensible default fallback.

Minor note: the capitalize class on line 36 is redundant since all labels are already capitalized in the switch cases, but it doesn't cause any issues.

apps/web/src/server/service/domain-service.ts (3)

90-106: LGTM!

The buildDomainPayload helper correctly maps all fields from the Domain model to the DomainPayload type, including proper ISO string conversion for dates and handling of nullable fields.


387-396: LGTM!

The emitDomainEvent helper properly wraps webhook emission with try/catch and structured logging, ensuring that webhook failures don't break domain operations. This is a good defensive pattern.


306-312: LGTM!

The webhook emission logic correctly detects status changes and emits the appropriate event type (domain.verified for successful verification, domain.updated otherwise). Comparing previousStatus against domainWithDns.status ensures events are only emitted when there's an actual change.

apps/web/src/server/api/routers/webhook.ts (1)

1-135: LGTM!

The webhook router is well-structured and follows TRPC best practices:

  • All endpoints are properly scoped by ctx.team.id for multi-tenant isolation
  • Input validation via zod schemas is comprehensive
  • Business logic is cleanly delegated to WebhookService
  • The teamProcedure wrapper ensures proper access control

Based on learnings, using TRPC for client-server communication aligns with the project's conventions.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)

1-72: LGTM on core component structure and data handling.

The component correctly handles loading states, retry functionality with proper cache invalidation, and payload reconstruction. The responseTimeMs != null check at line 134 properly handles the 0ms edge case.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (1)

137-162: LGTM on page setup and auto-selection logic.

The use of React 19's use() hook for async params is correct for Next.js 15. The useEffect for auto-selecting the first call appropriately guards against overwriting user selections.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)

28-68: LGTM on pagination and filtering implementation.

The cursor-based pagination with proper cursor stack management and filter reset logic is well implemented. The status filter correctly maps to the API query parameters.

apps/web/prisma/schema.prisma (2)

464-506: LGTM on webhook schema design.

The Webhook and WebhookCall models are well-designed with:

  • Appropriate cascade delete behavior for team/webhook relationships
  • SetNull for user deletion preserving webhook history
  • Composite index (teamId, webhookId, status) supporting common query patterns
  • Descending createdAt index for efficient recent-first ordering

450-462: LGTM on webhook status enums.

The WebhookStatus and WebhookCallStatus enums provide clear state management for the webhook lifecycle with appropriate values for active/paused/auto-disabled webhooks and pending/in-progress/delivered/failed/discarded calls.

packages/sdk/src/webhooks.ts (2)

263-272: Timing-safe comparison implementation is correct.

The safeEqual function properly uses timingSafeEqual from crypto module with length check guard. The early return on length mismatch is acceptable since length differences are already observable through timing of other operations.


45-137: LGTM on webhook verification implementation.

The Webhooks class provides a clean API following established patterns (similar to Stripe's SDK). Key security aspects are handled correctly:

  • HMAC-SHA256 signature verification
  • Timestamp validation with configurable tolerance
  • Timing-safe comparison to prevent timing attacks
  • Comprehensive error codes for debugging
apps/web/src/server/service/webhook-service.ts (3)

576-602: LGTM on Redis-based distributed locking.

The lock implementation correctly uses:

  • SET key value PX ttl NX for atomic lock acquisition
  • Lua script for safe release ensuring only the lock owner can release
  • Proper error handling in releaseLock

752-821: LGTM on response capture with streaming support.

The captureResponseText function properly:

  • Checks content-type before attempting to read text
  • Handles Content-Length header for early bailout on oversized responses
  • Uses streaming reader with proper truncation and reader.cancel() to release resources
  • Falls back to response.text() when streaming is unavailable

84-133: LGTM on webhook event emission.

The emit method correctly:

  • Queries for active webhooks matching the event type (or subscribed to all events via empty array)
  • Creates webhook call records before enqueueing to ensure delivery tracking
  • Handles the fan-out pattern for multiple webhooks

Comment on lines +29 to +39
const deliveredCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.DELIVERED,
).length;
const failedCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.FAILED,
).length;
const pendingCount = last7DaysCalls.filter(
(c) =>
c.status === WebhookCallStatus.PENDING ||
c.status === WebhookCallStatus.IN_PROGRESS,
).length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused computed values.

deliveredCount, failedCount, and pendingCount are computed but never rendered. This is dead code that wastes cycles on every render.

🔎 Proposed fix
   const calls = callsQuery.data?.items ?? [];
-  const last7DaysCalls = calls.filter(
-    (call) => new Date(call.createdAt) >= sevenDaysAgo,
-  );
-
-  const deliveredCount = last7DaysCalls.filter(
-    (c) => c.status === WebhookCallStatus.DELIVERED,
-  ).length;
-  const failedCount = last7DaysCalls.filter(
-    (c) => c.status === WebhookCallStatus.FAILED,
-  ).length;
-  const pendingCount = last7DaysCalls.filter(
-    (c) =>
-      c.status === WebhookCallStatus.PENDING ||
-      c.status === WebhookCallStatus.IN_PROGRESS,
-  ).length;

If these stats are intended to be displayed, implement the UI; otherwise remove to avoid unnecessary computation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const deliveredCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.DELIVERED,
).length;
const failedCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.FAILED,
).length;
const pendingCount = last7DaysCalls.filter(
(c) =>
c.status === WebhookCallStatus.PENDING ||
c.status === WebhookCallStatus.IN_PROGRESS,
).length;
const calls = callsQuery.data?.items ?? [];
const last7DaysCalls = calls.filter(
(call) => new Date(call.createdAt) >= sevenDaysAgo,
);
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx around
lines 29 to 39, the computed variables deliveredCount, failedCount, and
pendingCount are calculated but never used; remove these unused computations to
eliminate wasted work (delete the three const declarations and any now-unused
imports/types), or if the intent was to display these stats, hook them into the
component UI (add the corresponding JSX to render the counts where appropriate)
and ensure they are memoized if needed to avoid repeated computation.

Comment on lines +26 to +39
async function onConfirm(values: z.infer<typeof schema>) {
deleteWebhookMutation.mutate(
{ id: webhook.id },
{
onSuccess: async () => {
await utils.webhook.list.invalidate();
toast.success("Webhook deleted");
},
onError: (error) => {
toast.error(error.message);
},
},
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Async onConfirm should await the mutation.

The onConfirm function is marked async but uses mutate() which doesn't return a Promise. Since DeleteResource awaits onConfirm(), the function returns immediately while the mutation is still pending. Use mutateAsync() to properly chain the async behavior.

🔎 Proposed fix
   async function onConfirm(values: z.infer<typeof schema>) {
-    deleteWebhookMutation.mutate(
+    await deleteWebhookMutation.mutateAsync(
       { id: webhook.id },
-      {
-        onSuccess: async () => {
-          await utils.webhook.list.invalidate();
-          toast.success("Webhook deleted");
-        },
-        onError: (error) => {
-          toast.error(error.message);
-        },
-      },
     );
+    await utils.webhook.list.invalidate();
+    toast.success("Webhook deleted");
   }

Note: Error handling will be managed by the mutation's error state or you can wrap in try/catch if custom error handling is needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function onConfirm(values: z.infer<typeof schema>) {
deleteWebhookMutation.mutate(
{ id: webhook.id },
{
onSuccess: async () => {
await utils.webhook.list.invalidate();
toast.success("Webhook deleted");
},
onError: (error) => {
toast.error(error.message);
},
},
);
}
async function onConfirm(values: z.infer<typeof schema>) {
await deleteWebhookMutation.mutateAsync(
{ id: webhook.id },
);
await utils.webhook.list.invalidate();
toast.success("Webhook deleted");
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx around lines 26 to
39, onConfirm is async but calls deleteWebhookMutation.mutate which is not
promise-based, so the caller awaits prematurely; replace the mutate call with
await deleteWebhookMutation.mutateAsync({ id: webhook.id }) and either keep the
existing onSuccess/onError behavior on the mutation or handle success/error in a
try/catch around the await (e.g., await mutateAsync then await
utils.webhook.list.invalidate() and call toast.success, catch to call
toast.error).

Comment on lines +397 to +453
function emailStatusToEvent(status: EmailStatus): EmailWebhookEventType {
switch (status) {
case EmailStatus.QUEUED:
return "email.queued";
case EmailStatus.SENT:
return "email.sent";
case EmailStatus.DELIVERY_DELAYED:
return "email.delivery_delayed";
case EmailStatus.DELIVERED:
return "email.delivered";
case EmailStatus.BOUNCED:
return "email.bounced";
case EmailStatus.REJECTED:
return "email.rejected";
case EmailStatus.RENDERING_FAILURE:
return "email.rendering_failure";
case EmailStatus.COMPLAINED:
return "email.complained";
case EmailStatus.FAILED:
return "email.failed";
case EmailStatus.CANCELLED:
return "email.cancelled";
case EmailStatus.SUPPRESSED:
return "email.suppressed";
case EmailStatus.OPENED:
return "email.opened";
case EmailStatus.CLICKED:
return "email.clicked";
default:
return "email.queued";
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the EmailStatus enum definition to see all possible values
rg -n "enum EmailStatus|type EmailStatus" --type ts -A 20 -B 2

# Search for emailStatusToEvent usage to understand the context
rg -n "emailStatusToEvent" --type ts -B 3 -A 3

# Check if SCHEDULED is referenced anywhere in the codebase
rg -n "SCHEDULED" --type ts -B 2 -A 2

Repository: usesend/useSend

Length of output: 15706


🏁 Script executed:

# Check what EmailWebhookEventType values are available, especially for SCHEDULED
rg -n "EmailWebhookEventType|type EmailWebhookEventType" --type ts -A 20

# Check if there's any filtering that prevents SCHEDULED emails from reaching webhooks
rg -n "latestStatus.*SCHEDULED|SCHEDULED.*latestStatus" --type ts -B 3 -A 3

Repository: usesend/useSend

Length of output: 14109


🏁 Script executed:

# Get the full webhook-events.ts file to see the EmailEvents definition
cat -n packages/lib/src/webhook/webhook-events.ts | head -40

Repository: usesend/useSend

Length of output: 1189


Add explicit handling for EmailStatus.SCHEDULED in the switch statement.

The switch statement doesn't explicitly handle EmailStatus.SCHEDULED, which falls through to the default case returning "email.queued". Since there is no "email.scheduled" webhook event type available in the EmailEvents array, either filter out SCHEDULED emails before calling this function or throw an error to catch any unexpected status values at runtime.

🤖 Prompt for AI Agents
In apps/web/src/server/service/ses-hook-parser.ts around lines 397-428, the
switch lacks an explicit case for EmailStatus.SCHEDULED so it currently falls to
the default and returns "email.queued"; add an explicit case for
EmailStatus.SCHEDULED and handle it deterministically (either throw a clear
Error indicating SCHEDULED is unsupported here so callers must filter it out, or
map it to a defined event if you intentionally want to treat it as queued), then
update callers or document that SCHEDULED must be filtered before calling this
function.

Comment on lines +518 to +528
const updatedWebhook = await db.webhook.update({
where: { id: call.webhookId },
data: {
consecutiveFailures: {
increment: 1,
},
lastFailureAt: new Date(),
status:
call.webhook.consecutiveFailures + 1 >= WEBHOOK_AUTO_DISABLE_THRESHOLD
? WebhookStatus.AUTO_DISABLED
: call.webhook.status,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential race condition in auto-disable threshold check.

The threshold check at line 526 uses the pre-update value (call.webhook.consecutiveFailures + 1) while the atomic increment happens in the same update. If multiple webhook calls fail concurrently for the same webhook, each worker reads the same stale consecutiveFailures value, potentially delaying auto-disable.

This is low severity since the webhook will eventually be disabled, but for correctness you could use a transaction with SELECT ... FOR UPDATE or rely on the database-returned value.

🤖 Prompt for AI Agents
In apps/web/src/server/service/webhook-service.ts around lines 518-530, the code
computes the auto-disable status using the stale pre-update consecutiveFailures
value which can race under concurrent failures; wrap the read-and-update in a
transaction that locks the webhook row (SELECT ... FOR UPDATE), read the current
consecutiveFailures, compute newFailures = current + 1 and desired status based
on WEBHOOK_AUTO_DISABLE_THRESHOLD, then perform the update (or perform an atomic
update that returns the updated consecutiveFailures/status and use that returned
value) so the threshold decision is made against the locked/returned value.

stuff

webhook call changes

more stuff

stuff

stuff

more webhook stuff

add pnpm

more ui stuff

stuff

sdk stuff

stuff

webhook ui

stuff

stuff

stuff

fix: address code review issues in webhooks and SDK

- Fix colSpan values in webhook tables to match actual column counts
- Remove unused Card component imports from webhook-info.tsx
- Use WebhookStatus enum instead of string literals in webhook-status-badge
- Fix JSDoc example in usesend.ts to use correct constructEvent signature
- Cancel stream reader when truncating response in webhook-service
- Add error handling to Next.js webhook example in SDK README
- Move @usesend/lib to devDependencies and bundle at build time
- Use null check for responseTimeMs to handle 0ms response times

other changes

update lock
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docker/dev/compose.yml (1)

27-36: Service name has typo that needs fixing.

The compose.yml file is being created new with the correct usesend/local-ses-sns:latest image. However, the service name on line 27 is local-sen-sns, which is missing an 's' in "ses" and doesn't match the container name local-ses-sns on line 28. Fix the service name to local-ses-sns for consistency.

apps/web/src/server/service/contact-service.ts (1)

157-177: Emit webhook events for unsubscribeContact and subscribeContact.

Both functions update contact subscription status without emitting webhook events. Other contact mutation operations in this file (createContact, updateContact, deleteContact) all emit contact.updated or contact.created events. Add await emitContactEvent(updatedContact, "contact.updated", teamId) before returning in both functions to maintain consistency and notify webhook consumers of subscription status changes.

🤖 Fix all issues with AI agents
In `@apps/web/src/server/service/webhook-service.ts`:
- Around line 222-254: The createWebhook flow currently accepts any
syntactically valid URL; before persisting (in createWebhook) or before sending
requests (e.g., postWebhook), add SSRF/egress filtering: parse the URL (ensure
protocol is http or https), resolve the hostname to IP(s) (handle both A and
AAAA records and literal IPs), and reject if any resolved address falls into
private/reserved ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
localhost/127.0.0.0/8, ::1, fc00::/7, link-local, and cloud metadata like
169.254.169.254), or if the hostname is a localhost alias; implement this check
as a reusable helper (e.g., WebhookService.validateEgressUrl or a new helper
used by createWebhook and postWebhook), throw a UnsendApiError with FORBIDDEN
when blocked, and add unit tests covering hostnames, literal IPs, and DNS
resolving to private addresses.

In `@packages/sdk/README.md`:
- Around line 147-157: The snippet calling verifyWebhookSignature references
rawBody without showing how to obtain it; update the README example to
explicitly show how to derive rawBody (e.g., reading the request body as text or
buffer) before calling verifyWebhookSignature, mirroring the Next.js example’s
approach, and ensure the snippet uses the same header names (signatureHeader /
timestampHeader) from request.headers so readers can copy-paste without
ambiguity.
♻️ Duplicate comments (6)
apps/web/src/components/code-display.tsx (1)

59-67: Clean up setTimeout on unmount to prevent state updates on unmounted component.

This issue was previously identified. The timeout should be tracked and cleared on unmount.

♻️ Recommended fix using useRef
+"use client";
+
+import { useEffect, useState, useRef } from "react";
 import { BundledLanguage, codeToHtml } from "shiki";
 // ... rest of imports

 export function CodeDisplay({
   // ... props
 }: CodeDisplayProps) {
   const [html, setHtml] = useState<string>("");
   const [isLoading, setIsLoading] = useState(true);
   const [copied, setCopied] = useState(false);
+  const copyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (copyTimeoutRef.current) {
+        clearTimeout(copyTimeoutRef.current);
+      }
+    };
+  }, []);

   // ... useEffect for highlighting

   const handleCopy = async () => {
     try {
       await navigator.clipboard.writeText(code);
       setCopied(true);
-      setTimeout(() => setCopied(false), 2000);
+      if (copyTimeoutRef.current) {
+        clearTimeout(copyTimeoutRef.current);
+      }
+      copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
     } catch (error) {
       console.error("Failed to copy to clipboard:", error);
     }
   };
apps/web/src/server/service/ses-hook-parser.ts (1)

422-452: Handle EmailStatus.SCHEDULED explicitly (currently falls through).

EmailStatus.SCHEDULED still falls to the default and maps to "email.queued". This should be handled explicitly (either map intentionally or throw so callers filter it).

apps/web/src/server/service/webhook-service.ts (1)

516-526: Auto-disable threshold uses stale failure count under concurrency.

The status decision uses call.webhook.consecutiveFailures + 1, which can be stale if multiple calls fail concurrently for the same webhook. Prefer a transaction/locked read or use the updated value returned by the DB update.

apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

31-31: Remove unused testWebhook mutation.

The testWebhook mutation is declared but never used in this component. This appears to be leftover code that should be removed.

🔎 Proposed fix
 export function WebhookList() {
   const webhooksQuery = api.webhook.list.useQuery();
-  const testWebhook = api.webhook.test.useMutation();
   const setStatusMutation = api.webhook.setStatus.useMutation();
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)

212-226: Add error state handling for webhookQuery.

The component checks isLoading and !webhook but doesn't handle webhookQuery.isError. If the API call fails due to a network or server error, users will see "Webhook not found" instead of an appropriate error message.

🔎 Proposed fix
   if (webhookQuery.isLoading) {
     return (
       <div className="flex items-center justify-center h-screen">
         <p className="text-muted-foreground">Loading webhook...</p>
       </div>
     );
   }

+  if (webhookQuery.isError) {
+    return (
+      <div className="flex items-center justify-center h-screen">
+        <p className="text-destructive">
+          Failed to load webhook: {webhookQuery.error.message}
+        </p>
+      </div>
+    );
+  }
+
   if (!webhook) {

22-22: Remove unused CircleEllipsis import.

The CircleEllipsis icon is imported but never used in this component.

🔎 Proposed fix
 import {
   Edit3,
   Key,
   MoreVertical,
   Pause,
   Play,
   TestTube,
-  CircleEllipsis,
 } from "lucide-react";
🧹 Nitpick comments (6)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (2)

11-32: Consider adding exhaustive type checking for future-proofing.

The switch statement handles all current WebhookCallStatus values, but adding a default case with an exhaustiveness check would catch future enum additions at compile time.

♻️ Optional: Add exhaustiveness check
     case WebhookCallStatus.DISCARDED:
       badgeColor = "bg-gray-700/10 text-gray-400 border border-gray-400/10";
       label = "Discarded";
       break;
+    default: {
+      const _exhaustiveCheck: never = status;
+      break;
+    }
   }

34-40: Minor: capitalize class is redundant.

The labels are already capitalized strings (e.g., "Delivered", "In Progress"), so the capitalize Tailwind class has no effect.

♻️ Remove redundant class
-      className={`text-center w-[110px] rounded capitalize py-1 text-xs ${badgeColor}`}
+      className={`text-center w-[110px] rounded py-1 text-xs ${badgeColor}`}
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)

154-166: The formState parameter is unused in this render function.

The formState is destructured but not used in the URL field's render function, unlike the eventTypes field below which uses it for error display.

♻️ Suggested fix
             <FormField
               control={form.control}
               name="url"
-              render={({ field, formState }) => (
+              render={({ field }) => (
                 <FormItem>
references/webhook-architecture.md (1)

7-129: Consider adding a language identifier to the ASCII art code blocks.

The static analysis tool flags code blocks without language specifiers. For ASCII diagrams, you can use text or plaintext as the language identifier to satisfy linters while preserving the content.

♻️ Suggested fix for diagram blocks
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────────────────────┐

Apply similar changes to the other ASCII diagram blocks at lines 133, 173, 320, and 364.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)

163-170: Unnecessary React fragment wrapping a single element.

The fragment <>...</> around the Response Body section contains only a single <div> child, making the fragment redundant.

♻️ Suggested fix
         {call.responseText && (
-          <>
-            <div className="flex flex-col gap-3">
-              <h4 className="font-medium text-sm">Response Body</h4>
-              <CodeDisplay code={call.responseText} />
-            </div>
-          </>
+          <div className="flex flex-col gap-3">
+            <h4 className="font-medium text-sm">Response Body</h4>
+            <CodeDisplay code={call.responseText} />
+          </div>
         )}
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)

57-64: Consider extracting shared event selection logic.

The eventGroups definition and the event selection handlers (handleToggleAll, handleToggleGroup, handleToggleEvent) are duplicated between this file and add-webhook.tsx. Consider extracting these into a shared component or custom hook to reduce duplication and ensure consistent behavior.

♻️ Example extraction

Create a shared hook or component:

// webhook-event-selector.tsx
export function useWebhookEventSelection(initialEvents: WebhookEventType[]) {
  const [allEventsSelected, setAllEventsSelected] = useState(
    initialEvents.length === 0
  );
  // ... shared toggle logic
  return { allEventsSelected, handleToggleAll, handleToggleGroup, handleToggleEvent };
}

Comment on lines +222 to +254
public static async createWebhook(params: {
teamId: number;
userId: number;
url: string;
description?: string;
eventTypes: string[];
secret?: string;
}) {
const { isLimitReached, reason } = await LimitService.checkWebhookLimit(
params.teamId,
);

if (isLimitReached) {
throw new UnsendApiError({
code: "FORBIDDEN",
message: reason ?? "Webhook limit reached",
});
}

const secret = params.secret ?? WebhookService.generateSecret();

return db.webhook.create({
data: {
teamId: params.teamId,
url: params.url,
description: params.description,
secret,
eventTypes: params.eventTypes,
status: WebhookStatus.ACTIVE,
createdByUserId: params.userId,
},
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for webhook URL validation or SSRF protections
rg -n "webhook.*url|validate.*url|ssrf|private ip|blocklist|allowlist" -t ts --max-count=50

Repository: usesend/useSend

Length of output: 810


🏁 Script executed:

#!/bin/bash
# Find the webhook-service.ts file and check its size
find . -name "webhook-service.ts" -type f
wc -l $(find . -name "webhook-service.ts" -type f)

Repository: usesend/useSend

Length of output: 160


🏁 Script executed:

#!/bin/bash
# Look at the postWebhook method around lines 666-742
sed -n '666,742p' $(find . -name "webhook-service.ts" -type f)

Repository: usesend/useSend

Length of output: 1980


🏁 Script executed:

#!/bin/bash
# Search for any URL validation utilities in the codebase
rg -n "validateUrl|validateWebhookUrl|isValidUrl" -t ts

Repository: usesend/useSend

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Search for fetch calls related to webhooks
rg -n "fetch.*webhook|webhook.*fetch" -t ts -A 3

Repository: usesend/useSend

Length of output: 400


🏁 Script executed:

#!/bin/bash
# Search for webhook-related tRPC procedures or API endpoints
rg -n "createWebhook|webhook.*procedure|webhook.*router" -t ts -B 2 -A 5 | head -100

Repository: usesend/useSend

Length of output: 4164


🏁 Script executed:

#!/bin/bash
# Look for input validation schemas for webhook endpoints
rg -n "webhook.*schema|createWebhookInput|zod.*webhook" -t ts -A 5

Repository: usesend/useSend

Length of output: 513


🏁 Script executed:

#!/bin/bash
# Find where webhookSchema is defined
rg -n "webhookSchema|webhook.*z\.object" -t ts -A 10 | head -60

Repository: usesend/useSend

Length of output: 2634


🏁 Script executed:

#!/bin/bash
# Check the webhook router file for input validation
cat -n apps/web/src/server/api/routers/webhook.ts | head -50

Repository: usesend/useSend

Length of output: 1951


Add SSRF protection to webhook URLs.

The URL validation (z.string().url()) only checks format validity, not content. A tenant can create webhooks pointing to private IPs, localhost, or cloud metadata endpoints (e.g., http://127.0.0.1:8080, http://169.254.169.254/), which would then be fetched by the server, exposing internal services. Implement egress filtering before postWebhook executes the request: validate that URLs use only http/https, block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and reject localhost/reserved addresses.

🤖 Prompt for AI Agents
In `@apps/web/src/server/service/webhook-service.ts` around lines 222 - 254, The
createWebhook flow currently accepts any syntactically valid URL; before
persisting (in createWebhook) or before sending requests (e.g., postWebhook),
add SSRF/egress filtering: parse the URL (ensure protocol is http or https),
resolve the hostname to IP(s) (handle both A and AAAA records and literal IPs),
and reject if any resolved address falls into private/reserved ranges
(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, localhost/127.0.0.0/8, ::1,
fc00::/7, link-local, and cloud metadata like 169.254.169.254), or if the
hostname is a localhost alias; implement this check as a reusable helper (e.g.,
WebhookService.validateEgressUrl or a new helper used by createWebhook and
postWebhook), throw a UnsendApiError with FORBIDDEN when blocked, and add unit
tests covering hostnames, literal IPs, and DNS resolving to private addresses.

- Remove unused Trash2 import from webhook-list.tsx
- Centralize WEBHOOK_EVENT_VERSION in @usesend/lib
- Add webhook.test event type with proper payload typing
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/web/src/app/`(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx:
- Around line 3-4: The file imports and uses formatDate from date-fns which
doesn't exist; replace the import of formatDate with format and update all
usages of formatDate (e.g., any calls like formatDate(...)) to call format(...)
instead; ensure the symbol names referenced are updated (import: format, usages:
replace formatDate with format) so date formatting works with date-fns v4.1.0.

In `@apps/web/src/server/service/webhook-service.ts`:
- Around line 530-543: In the db.webhookCall.update call that updates
status/attempt/nextAttemptAt/lastError (the data block around
db.webhookCall.update), change the response fields so they are set to null on no
response rather than using the "?? undefined" pattern; specifically set
responseStatus, responseTimeMs, and responseText to null when there is no new
value so previous attempt data is cleared (preserve existing WebhookCallStatus
logic and attempt/nextAttemptAt handling).
♻️ Duplicate comments (1)
apps/web/src/server/service/webhook-service.ts (1)

222-254: Add SSRF/egress validation for webhook URLs.

createWebhook accepts any valid URL string; without egress filtering, tenants could target localhost/private IPs or metadata endpoints. Add URL validation (http/https only) plus DNS/IP allow/deny checks (localhost, RFC1918, link-local, metadata IPs) before persisting and before postWebhook sends.

Comment on lines +3 to +4
import { formatDate } from "date-fns";
import { RefreshCw } from "lucide-react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

date-fns v4.1.0 formatDate export API documentation

💡 Result:

Short answer: date-fns v4.1.0 does not export a function named formatDate — the formatter API function is named format (and related helpers like formatISO, formatRelative, etc.). v4.1.0 added missing time‑zone support to format/formatISO/formatISO9075/formatRelative/formatRFC3339. [1][2]

References

  • v4.1.0 release notes (time‑zone support added). [1]
  • API docs for format (signature and examples). [2]
  • npm listing for date-fns v4.1.0. [3]

Sources
[1] GitHub Releases — date-fns v4.1.0.
[2] date-fns (v4.1.0) API docs (format).
[3] npm: date-fns v4.1.0.


🏁 Script executed:

# Examine the file to see imports and usages
cat -n apps/web/src/app/\(dashboard\)/webhooks/\[webhookId\]/webhook-call-details.tsx | head -20

Repository: usesend/useSend

Length of output: 915


🏁 Script executed:

# Check for all occurrences of formatDate in the file
rg "formatDate" apps/web/src/app/\(dashboard\)/webhooks/\[webhookId\]/webhook-call-details.tsx -n

Repository: usesend/useSend

Length of output: 171


Use format instead of formatDate from date-fns.

formatDate is not exported from date-fns v4.1.0. The correct function is format.

🛠️ Proposed fix
-import { formatDate } from "date-fns";
+import { format } from "date-fns";

Also applies to: 113

🤖 Prompt for AI Agents
In `@apps/web/src/app/`(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
around lines 3 - 4, The file imports and uses formatDate from date-fns which
doesn't exist; replace the import of formatDate with format and update all
usages of formatDate (e.g., any calls like formatDate(...)) to call format(...)
instead; ensure the symbol names referenced are updated (import: format, usages:
replace formatDate with format) so date formatting works with date-fns v4.1.0.

Comment on lines +530 to +543
await db.webhookCall.update({
where: { id: call.id },
data: {
status:
attempt >= WEBHOOK_MAX_ATTEMPTS
? WebhookCallStatus.FAILED
: WebhookCallStatus.PENDING,
attempt,
nextAttemptAt,
lastError: errorMessage,
responseStatus: responseStatus ?? undefined,
responseTimeMs: responseTimeMs ?? undefined,
responseText: responseText ?? undefined,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear response fields on error to avoid stale data.

Using ?? undefined keeps prior attempt values when a later attempt has no response data (e.g., timeout). Set null so the call reflects the latest attempt.

🛠️ Proposed fix
-        responseStatus: responseStatus ?? undefined,
-        responseTimeMs: responseTimeMs ?? undefined,
-        responseText: responseText ?? undefined,
+        responseStatus: responseStatus ?? null,
+        responseTimeMs: responseTimeMs ?? null,
+        responseText: responseText ?? null,
🤖 Prompt for AI Agents
In `@apps/web/src/server/service/webhook-service.ts` around lines 530 - 543, In
the db.webhookCall.update call that updates
status/attempt/nextAttemptAt/lastError (the data block around
db.webhookCall.update), change the response fields so they are set to null on no
response rather than using the "?? undefined" pattern; specifically set
responseStatus, responseTimeMs, and responseText to null when there is no new
value so previous attempt data is cleared (preserve existing WebhookCallStatus
logic and attempt/nextAttemptAt handling).

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/docs/guides/webhooks.mdx">

<violation number="1" location="apps/docs/guides/webhooks.mdx:260">
P2: Guard against missing signature/timestamp headers before calling `verifyWebhook`; otherwise the example can throw at runtime or validate an invalid signature when a header is absent.</violation>
</file>

<file name="references/webhook-architecture.md">

<violation number="1" location="references/webhook-architecture.md:233">
P2: The TypeScript union example is now invalid: `("webhook.test");` is a standalone expression, not a union member. This makes the docs misleading and the snippet won’t compile if copied. Add it as a union member instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const signature = request.headers.get("X-UseSend-Signature");
const timestamp = request.headers.get("X-UseSend-Timestamp");

const isValid = verifyWebhook(secret, rawBody, signature, timestamp);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard against missing signature/timestamp headers before calling verifyWebhook; otherwise the example can throw at runtime or validate an invalid signature when a header is absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/docs/guides/webhooks.mdx, line 260:

<comment>Guard against missing signature/timestamp headers before calling `verifyWebhook`; otherwise the example can throw at runtime or validate an invalid signature when a header is absent.</comment>

<file context>
@@ -0,0 +1,486 @@
+const signature = request.headers.get("X-UseSend-Signature");
+const timestamp = request.headers.get("X-UseSend-Timestamp");
+
+const isValid = verifyWebhook(secret, rawBody, signature, timestamp);
+```
+
</file context>
Fix with Cubic

"email.opened" |
"email.clicked";

// Test events
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The TypeScript union example is now invalid: ("webhook.test"); is a standalone expression, not a union member. This makes the docs misleading and the snippet won’t compile if copied. Add it as a union member instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At references/webhook-architecture.md, line 233:

<comment>The TypeScript union example is now invalid: `("webhook.test");` is a standalone expression, not a union member. This makes the docs misleading and the snippet won’t compile if copied. Add it as a union member instead.</comment>

<file context>
@@ -229,6 +229,9 @@ Defined in `packages/lib/src/webhook/webhook-events.ts`:
   "email.opened" |
   "email.clicked";
+
+// Test events
+("webhook.test");

</file context>


</details>

<a href="https://www.cubic.dev/action/fix/violation/879f1301-0689-4e12-a8b9-0127eda1ee96" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://cubic.dev/buttons/fix-with-cubic-dark.svg">
    <source media="(prefers-color-scheme: light)" srcset="https://cubic.dev/buttons/fix-with-cubic-light.svg">
    <img alt="Fix with Cubic" src="https://cubic.dev/buttons/fix-with-cubic-dark.svg">
  </picture>
</a>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/docs/guides/webhooks.mdx`:
- Around line 256-261: The example uses request.headers.get(...) which may
return null but verifyWebhook(secret, rawBody, signature, timestamp) requires
strings; update the example around the variables signature and timestamp to
handle nulls before calling verifyWebhook — e.g., retrieve them via
request.headers.get("X-UseSend-Signature") and
request.headers.get("X-UseSend-Timestamp"), validate or coerce to string (or
throw/return an error if missing) so that the values passed to verifyWebhook are
non-null strings; ensure you reference the same variable names (signature,
timestamp, secret, rawBody, verifyWebhook) in the example.

In `@references/webhook-architecture.md`:
- Line 336: Fix the typo in the signature pseudocode: replace the incorrect
"HMAC - SHA256" with the correct algorithm name "HMAC-SHA256" in the line that
computes const signature; ensure the call reads HMAC-SHA256(secret,
`${timestamp}.${JSON.stringify(body)}`) so the symbols signature, secret,
timestamp, and body are used exactly as intended.
🧹 Nitpick comments (2)
references/webhook-architecture.md (1)

7-7: Add language specifiers to fenced code blocks.

Markdownlint flags these code blocks as missing a language specifier. For ASCII diagrams and pseudocode, use text or plaintext to satisfy the linter while preserving formatting.

Suggested fix
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────────────────────┐

Apply similarly to lines 133, 173, 323, and 367.

Also applies to: 133-133, 173-173, 323-323, 367-367

apps/docs/guides/webhooks.mdx (1)

329-345: Accordion list items may not render correctly.

The troubleshooting accordion content uses inline dashes for list items without line breaks, which may render as a single paragraph rather than a proper list in MDX.

Suggested fix for proper list rendering
   <Accordion title="Webhook not receiving events">
-    - Verify your endpoint URL is correct and publicly accessible - Check that
-    your endpoint returns a 2xx status code - Ensure the webhook is set to
-    ACTIVE status in the dashboard - Check if the webhook was auto-disabled due
-    to consecutive failures
+    - Verify your endpoint URL is correct and publicly accessible
+    - Check that your endpoint returns a 2xx status code
+    - Ensure the webhook is set to ACTIVE status in the dashboard
+    - Check if the webhook was auto-disabled due to consecutive failures
   </Accordion>
   <Accordion title="Signature verification failing">
-    - Use the raw request body, not parsed JSON - Ensure you're using the
-    correct webhook secret - Check that the timestamp hasn't expired (5 minute
-    window) - Verify you're computing the HMAC correctly: `HMAC-SHA256(secret,
-    "${timestamp}.${rawBody}")`
+    - Use the raw request body, not parsed JSON
+    - Ensure you're using the correct webhook secret
+    - Check that the timestamp hasn't expired (5 minute window)
+    - Verify you're computing the HMAC correctly: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`
   </Accordion>

Apply similar formatting to all accordion items.

Comment on lines +256 to +261
// Usage
const signature = request.headers.get("X-UseSend-Signature");
const timestamp = request.headers.get("X-UseSend-Timestamp");

const isValid = verifyWebhook(secret, rawBody, signature, timestamp);
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle potential null values from headers.get().

The headers.get() method can return null, but verifyWebhook expects string arguments. Users copying this example verbatim may encounter TypeScript errors or runtime issues.

Suggested fix
 // Usage
 const signature = request.headers.get("X-UseSend-Signature");
 const timestamp = request.headers.get("X-UseSend-Timestamp");

+if (!signature || !timestamp) {
+  return new Response("Missing signature headers", { status: 400 });
+}
+
 const isValid = verifyWebhook(secret, rawBody, signature, timestamp);
🤖 Prompt for AI Agents
In `@apps/docs/guides/webhooks.mdx` around lines 256 - 261, The example uses
request.headers.get(...) which may return null but verifyWebhook(secret,
rawBody, signature, timestamp) requires strings; update the example around the
variables signature and timestamp to handle nulls before calling verifyWebhook —
e.g., retrieve them via request.headers.get("X-UseSend-Signature") and
request.headers.get("X-UseSend-Timestamp"), validate or coerce to string (or
throw/return an error if missing) so that the values passed to verifyWebhook are
non-null strings; ensure you reference the same variable names (signature,
timestamp, secret, rawBody, verifyWebhook) in the example.

Signature computation:

```typescript
const signature = HMAC - SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in signature computation pseudocode.

There are extraneous spaces in HMAC - SHA256 which should be HMAC-SHA256.

Suggested fix
-const signature = HMAC - SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);
+const signature = HMAC-SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const signature = HMAC - SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);
const signature = HMAC-SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);
🤖 Prompt for AI Agents
In `@references/webhook-architecture.md` at line 336, Fix the typo in the
signature pseudocode: replace the incorrect "HMAC - SHA256" with the correct
algorithm name "HMAC-SHA256" in the line that computes const signature; ensure
the call reads HMAC-SHA256(secret, `${timestamp}.${JSON.stringify(body)}`) so
the symbols signature, secret, timestamp, and body are used exactly as intended.

@KMKoushik KMKoushik merged commit 8676965 into main Jan 18, 2026
11 of 12 checks passed
@KMKoushik KMKoushik deleted the km/2025-11-17-webhook branch January 18, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants