Skip to content

Conversation

@marcrupt
Copy link
Collaborator

@marcrupt marcrupt commented Nov 18, 2025

Summary by CodeRabbit

  • New Features

    • Full webhook management: create, list, view, test, and delete via API and UI; new Webhooks dashboard and sidebar link.
    • Recent webhook delivery attempts shown in UI; attempts endpoint available and attempts deduplicated by target URL.
  • Bug Fixes / UX

    • DNS record host names (MX/TXT/DMARC) now derive correctly for multi-label domains.
  • Documentation

    • Added Webhooks API docs and Quick Start API examples; updated SDK usage snippets and import.
  • Tests

    • SDK webhook tests added.

@marcrupt marcrupt requested a review from ahmedmawiri November 18, 2025 03:07
@vercel
Copy link

vercel bot commented Nov 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
sendook Ready Ready Preview Comment Nov 18, 2025 4:41pm
sendook-app Ready Ready Preview Comment Nov 18, 2025 4:41pm

@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Walkthrough

Adds organization-scoped webhook CRUD and test endpoints, UI for managing webhooks, backend webhook dispatch deduplication with payload-wrapping and timestamps, attempts listing, Node SDK webhook methods/types and tests, OpenAPI entries, README docs/examples updates, and DNS host-name derivation tweaks for multi-label domains.

Changes

Cohort / File(s) Summary
Docs & README
README.md, landing/content/1.docs/3.api/1.index.md, landing/content/1.docs/3.api/7.webhooks.md
Adds Webhooks API docs and Quick Start API examples (inbox creation, send, webhook creation); updates TypeScript SDK import to @sendook/node and example payload fields to text/html.
Backend — Webhook Dispatch & Attempts
api/controllers/WebhookAttemptController.ts, api/routes/v1/webhooks/attempts.ts
Deduplicates webhook targets by URL, wraps stored attempt payloads as { event, payload }, adds timestamps to attempts, extends sendWebhookEvent payload typing, and exposes API to fetch recent attempts (limit 10) for an org+webhook.
Backend — Webhook Data Helpers
api/controllers/WebhookController.ts
Adds helpers: getWebhookById, getWebhooksByOrganizationId, getWebhookByOrganizationIdAndId, and deleteWebhookByOrganizationIdAndId (with ObjectId conversion).
Routing — Organization & V1 Mounts
api/routes/organizations.ts, api/routes/v1/index.ts
Mounts webhooks router under organization-scoped and v1 paths to expose the new webhook endpoints.
Routing — Webhooks Endpoints
api/routes/v1/webhooks/index.ts
New router providing list/create/get/delete webhooks, /:webhookId/test to send test events, input validation, and 404 handling; wires attempts sub-router.
Routing — Webhook Test Receiver
api/routes/webhooks.ts, api/routes/webhooks/test.ts
Adds /test receiver route that responds 200 for test deliveries (no body processing).
Frontend — Dashboard & Page
app/app/components/dashboard/DashboardShell.vue, app/app/pages/webhooks.vue
Adds Webhooks nav link and implements a full Webhooks management UI: create/delete/test flows, details modal, attempts table, validation, and error handling.
Node SDK — Client, Types & Tests
node-sdk/index.ts, node-sdk/types/sendook-api.d.ts, node-sdk/tests/webhookAPI.spec.ts
Adds webhook methods on SendookAPI (list, create, test, get, delete), associated TypeScript interfaces, OpenAPI entries (node-sdk/openapi.yaml), and SDK tests exercising webhook CRUD and test flows.
Domains DNS Host Logic
api/routes/v1/domains/index.ts
Adjusts MX/TXT/DMARC record name derivation: when domain has subdomain labels, derive host by removing last two labels; otherwise use "@" or _dmarc accordingly.

Sequence Diagram(s)

sequenceDiagram
    rect rgb(240,248,255)
    participant User as User (Dashboard)
    participant Front as Frontend Webhooks Page
    participant API as API /v1/webhooks
    participant Controller as WebhookController
    participant DB as Database
    end

    User->>Front: Click "Add webhook"
    Front->>Front: Validate URL & events
    Front->>API: POST /v1/webhooks { url, events }
    API->>Controller: createWebhook(orgId, payload)
    Controller->>DB: insert webhook
    DB-->>Controller: created
    Controller-->>API: 201 {webhook}
    API-->>Front: 201 response
    Front->>API: GET /v1/webhooks
    API->>DB: find webhooks for org
    DB-->>API: webhooks list
    API-->>Front: webhooks
Loading
sequenceDiagram
    rect rgb(245,255,240)
    participant API as API /v1/webhooks/:id/test
    participant AttemptCtrl as WebhookAttemptController
    participant Ext as External Webhook Endpoint
    participant DB as Database
    end

    API->>AttemptCtrl: sendWebhookEvent({ test: "..." })
    AttemptCtrl->>AttemptCtrl: fetch webhooks for org
    AttemptCtrl->>AttemptCtrl: deduplicate targets by URL
    AttemptCtrl->>AttemptCtrl: wrap payload => { event, payload } and add timestamp
    AttemptCtrl->>Ext: POST { event, inboxId?, messageId?, payload }
    Ext-->>AttemptCtrl: response (2xx/4xx/5xx)
    AttemptCtrl->>DB: save WebhookAttempt record with timestamp and payload
    DB-->>AttemptCtrl: saved
    AttemptCtrl-->>API: aggregated result
    API-->>Caller: 200 OK
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Review focus:
    • app/app/pages/webhooks.vue — state management, validation, accessibility, and attempts rendering.
    • api/controllers/WebhookAttemptController.ts — deduplication correctness, payload wrapping shape, timestamp assignment, and error handling for external calls.
    • Route mounting (api/routes/organizations.ts, api/routes/v1/index.ts) and route parameter/authorization consistency.
    • SDK additions (node-sdk/index.ts, node-sdk/types/sendook-api.d.ts) and test reliability.

Possibly related PRs

  • Readme improvements #9 — Overlaps README Quick Start and SDK example edits (TypeScript import and payload fields).
  • inbounds and parsing #3 — Related webhook event emission and webhook events surface changes affecting webhook dispatch and event types.

Suggested reviewers

  • ahmedmawiri

Poem

🐇 I hop with joy, a webhook sprout,

URLs stored and tests sent out,
Dashboard clicks and timestamps bright,
Attempts logged in morning light,
Code hops forward — give a shout!

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 'Webhook APIs and supporting documentation' clearly and concisely summarizes the main changes in this pull request, which adds comprehensive webhook functionality and related documentation across multiple components.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch marc/rup-898-update-readme-for-sendreceive

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

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
api/controllers/WebhookAttemptController.ts (1)

22-47: sendWebhookEvent + test route currently broadcast tests to all webhooks for the event

sendWebhookEvent loads webhooks by { organizationId, event } and then iterates over the (deduped) list:

const webhooks = await getWebhooksByOrganizationIdAndEvent({ organizationId, event });
// ...
const webhooksWithoutDuplicates = webhooks.filter(/* dedupe by url */);

for (const webhook of webhooksWithoutDuplicates) {
  // send axios.post(webhook.url, …)
}

The /v1/webhooks/:webhookId/test route, however, calls this helper without constraining which webhook to send to:

await sendWebhookEvent({
  organizationId: req.organization._id.toString(),
  event: "message.received",
  payload: { test: "test" },
});

Effectively, hitting POST /webhooks/:webhookId/test will send a test payload to every webhook in that organization that listens to "message.received", not just the :webhookId the caller asked to test. That’s unintuitive and can easily result in test traffic being delivered to real production endpoints.

Consider tightening this behavior so the test endpoint only sends to the selected webhook, for example by:

  • Extending sendWebhookEvent to accept an optional webhookIds (or single webhookId) filter and using that in the query, or
  • Implementing a small dedicated “send test webhook” helper that posts only to the resolved webhook from the route and records a single WebhookAttempt.

Separately (optional), you may want to:

  • Add a per-request timeout to the axios.post call and/or run webhook deliveries concurrently (e.g., Promise.all) to avoid one slow endpoint blocking all others.

Also applies to: 32-37, 48-63

🧹 Nitpick comments (3)
api/routes/webhooks/test.ts (1)

5-8: Replace console.log with proper logging framework.

Using console.log directly is not ideal for production environments. Consider using a structured logging framework that's likely used elsewhere in the codebase (such as Winston, Pino, or similar).

Additionally, consider adding basic error handling and potentially some validation or rate limiting to prevent abuse of this public endpoint:

 router.post("/", async (req, res) => {
-  console.log("Received test webhook", req.body);
-  return res.status(200).send();
+  try {
+    // Use proper logging framework (e.g., logger.info)
+    logger.info("Received test webhook", { body: req.body });
+    return res.status(200).json({ received: true });
+  } catch (error) {
+    logger.error("Error processing test webhook", { error });
+    return res.status(500).json({ error: "Internal server error" });
+  }
 });
node-sdk/tests/webhookAPI.spec.ts (1)

8-65: Make webhook SDK tests self-contained and avoid ts-ignore

The new tests cover the webhook surface well, but there are a couple of robustness nits:

  • listWebhooks assumes there is at least one existing webhook and asserts webhooks.length > 0. That depends on test order and/or external state. Prefer making it self-contained, e.g.:
    • Create a webhook within the test (or in a shared setup), then call list() and assert that the returned array contains that webhook (and optionally clean it up).
  • The // @ts-ignore for the bun:test import suggests TypeScript isn’t aware of Bun’s test types. It would be cleaner to configure Bun’s type definitions in tsconfig (or add a small global typing shim) instead of suppressing type checking at the import site.

These aren’t blockers but will make the test suite more reliable and maintainable.

api/controllers/WebhookController.ts (1)

22-47: Controller helpers look correct; consider guarding against invalid ObjectIds

The new helpers (getWebhookById, getWebhooksByOrganizationId, getWebhookByOrganizationIdAndId, deleteWebhookByOrganizationIdAndId) correctly scope queries by organizationId and cast string ids to mongoose.Types.ObjectId. This matches how the router uses them.

One optional improvement: if a caller passes a non-ObjectId webhookId (e.g. a random string), new mongoose.Types.ObjectId(id) will throw and bubble up as a 500 instead of a clean 400. In other controllers you may already have this pattern, but if you want more defensive behavior you could:

  • Validate ids at the route layer (e.g. via express-validator and a custom isMongoId check), or
  • Add a small helper that returns null when id is not a valid ObjectId string.

Not blocking, but worth considering for more predictable API errors.

Also applies to: 62-73

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 496d16c and 8761430.

📒 Files selected for processing (16)
  • README.md (2 hunks)
  • api/controllers/WebhookAttemptController.ts (3 hunks)
  • api/controllers/WebhookController.ts (2 hunks)
  • api/routes/organizations.ts (2 hunks)
  • api/routes/v1/index.ts (2 hunks)
  • api/routes/v1/webhooks/index.ts (1 hunks)
  • api/routes/webhooks.ts (1 hunks)
  • api/routes/webhooks/test.ts (1 hunks)
  • app/app/components/dashboard/DashboardShell.vue (1 hunks)
  • app/app/pages/webhooks.vue (1 hunks)
  • landing/content/1.docs/3.api/1.index.md (1 hunks)
  • landing/content/1.docs/3.api/7.webhooks.md (1 hunks)
  • node-sdk/index.ts (2 hunks)
  • node-sdk/openapi.yaml (1 hunks)
  • node-sdk/tests/webhookAPI.spec.ts (1 hunks)
  • node-sdk/types/sendook-api.d.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
node-sdk/index.ts (1)
node-sdk/types/sendook-api.d.ts (1)
  • WebhookMethods (109-115)
api/routes/v1/webhooks/index.ts (4)
api/controllers/WebhookController.ts (4)
  • getWebhooksByOrganizationId (26-34)
  • createWebhook (5-20)
  • getWebhookByOrganizationIdAndId (36-47)
  • deleteWebhookByOrganizationIdAndId (62-73)
api/middlewares/expressValidatorMiddleware.ts (1)
  • expressValidatorMiddleware (4-16)
api/models/Webhook.ts (1)
  • WebhookEvents (3-13)
api/controllers/WebhookAttemptController.ts (1)
  • sendWebhookEvent (11-64)
api/controllers/WebhookController.ts (1)
api/models/Webhook.ts (1)
  • Webhook (15-22)
api/controllers/WebhookAttemptController.ts (3)
api/models/Inbox.ts (1)
  • Inbox (3-11)
api/models/Message.ts (1)
  • Message (12-36)
api/models/WebhookAttempt.ts (1)
  • WebhookAttempt (3-16)
🪛 Checkov (3.2.334)
node-sdk/openapi.yaml

[medium] 57-61: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (8)
landing/content/1.docs/3.api/1.index.md (1)

83-83: LGTM!

Documentation link addition is consistent with the existing format and properly integrates the new Webhooks API into the resources list.

node-sdk/types/sendook-api.d.ts (1)

5-16: LGTM!

The webhook type definitions follow the established patterns used for other resources (API Keys, Domains, etc.) and provide clear type signatures for all webhook operations.

Also applies to: 109-115

api/routes/webhooks.ts (1)

3-8: LGTM!

Router mounting follows the established pattern used for the SES router and properly integrates the test webhook endpoint into the routing hierarchy.

app/app/components/dashboard/DashboardShell.vue (1)

52-53: LGTM!

The Webhooks navigation item is properly integrated into the dashboard menu and will work correctly with the existing active route highlighting logic.

api/routes/v1/index.ts (1)

8-23: LGTM!

The webhooks router is properly mounted in the v1 API with authentication middleware applied, following the same pattern as other resource routers.

landing/content/1.docs/3.api/7.webhooks.md (1)

1-194: LGTM!

The webhook documentation is comprehensive and well-structured, providing clear guidance on:

  • Available event types
  • All CRUD operations with request/response examples
  • Webhook payload format and structure
  • Delivery behavior and best practices

This will be valuable for developers integrating with the Webhooks API.

node-sdk/index.ts (1)

280-330: LGTM!

The webhook methods implementation is consistent with existing SDK patterns for other resources:

  • Proper use of axios with Authorization headers
  • Correct HTTP verbs for each operation
  • Clean URL construction following the v1 API structure
  • Returns response.data for easy consumption

The implementation correctly mirrors the WebhookMethods interface and follows the established SDK conventions.

api/routes/organizations.ts (1)

7-7: Organization-scoped webhooks routing looks consistent

Importing webhooksRouter and mounting it at /:organizationId/webhooks fits the existing pattern for api_keys, inboxes, and domains, and works with mergeParams: true in the v1 webhooks router.

No issues from this change.

Also applies to: 27-30

Comment on lines +27 to +47
router.post(
"/",
body("url").isString().trim(),
body("events").isArray().notEmpty(),
expressValidatorMiddleware,
async (
req: Request<
{ organizationId: string },
{},
{ url: string; events: (typeof WebhookEvents)[number][] }
>,
res: Response
) => {
const webhook = await createWebhook({
organizationId: req.organization._id.toString(),
url: req.body.url,
events: req.body.events,
});
return res.json(webhook);
}
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten test route targeting and creation semantics for webhooks

A few points in this router worth adjusting:

  1. POST /:webhookId/test hits all webhooks for the event

    As noted in the controller review, this route verifies the target webhook exists, but then calls:

    await sendWebhookEvent({
      organizationId: req.organization._id.toString(),
      event: "message.received",
      payload: { test: "test" },
    });

    Since sendWebhookEvent selects webhooks by { organizationId, event }, this will send a test payload to every "message.received" webhook in the organization, not just :webhookId. That’s surprising given the route shape and can cause unintended traffic to unrelated endpoints.

    Suggestion: after fetching webhook by organization + id, either:

    • Pass an explicit webhook filter into sendWebhookEvent (after updating its signature to support that), or
    • Call a dedicated helper that posts only to this webhook.url and records a single WebhookAttempt.
  2. POST / should ideally return 201 for creation

    createWebhook returns a newly created resource, but the route currently does return res.json(webhook); (status 200). For a creation endpoint, using res.status(201).json(webhook); would be more idiomatic and match the OpenAPI spec’s 201 response for createWebhook.

  3. Optional: validate events against WebhookEvents

    The body validation only checks events is a non-empty array:

    body("events").isArray().notEmpty(),

    but doesn’t enforce that each entry is one of WebhookEvents. Adding a custom validator (e.g., .custom((events) => events.every(e => WebhookEvents.includes(e)))) would prevent storing unsupported event types that will never fire.

Items (2) and (3) are nice-to-have refactors; item (1) is the important behavioral fix.

Also applies to: 49-88, 90-109

Comment on lines +20 to +36
<template v-else>
<div v-if="verificationMessage" class="banner banner-success">
{{ verificationMessage }}
</div>
<div v-if="verificationError" class="banner banner-error" role="alert">
{{ verificationError }}
</div>

<section v-if="webhooks.length === 0" class="placeholder-card">
<h2>Connect your webhook</h2>
<p>
Webhooks will appear here once they are configured. Add a webhook URL and select events to
receive real-time notifications.
</p>
</section>

<section v-else class="domains-grid">
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface load failures to the user instead of silently showing an empty state

loadWebhooks currently handles failures like this:

try {
  const response = await fetch(/* … */);
  // …
  webhooks.value = /* … */;
} catch (error) {
  console.error('Failed to load webhooks', error);
  errorMessage.value = 'Unable to load webhooks. Please try again.';
  webhooks.value = [];
} finally {
  loading.value = false;
}

But in the template:

  • The top-level alerts use verificationMessage / verificationError.
  • errorMessage is only rendered inside the “Add webhook” dialog.

Result: if the initial fetch fails, the user sees the “Connect your webhook” empty state (because webhooks = [] and loading = false) with no visible error, even though something went wrong.

To avoid this misleading UX, consider:

  • Using verificationError (which already drives a page-level alert) for load errors:
-  catch (error) {
-    console.error('Failed to load webhooks', error);
-    errorMessage.value = 'Unable to load webhooks. Please try again.';
-    webhooks.value = [];
-  }
+  catch (error) {
+    console.error('Failed to load webhooks', error);
+    verificationError.value = 'Unable to load webhooks. Please try again.';
+    webhooks.value = [];
+  }
  • Optionally removing or repurposing verificationMessage / verificationError if they were copied from the domains page and are otherwise unused.

The rest of the page (list/create/delete/test flows and API calls) looks coherent and aligned with the backend routes.

Also applies to: 249-316

🤖 Prompt for AI Agents
In app/app/pages/webhooks.vue around lines 20-36 (and similarly 249-316), the
page-level error created in loadWebhooks is stored in errorMessage which is
never rendered on this page; update the loadWebhooks catch block to set
verificationError.value with the user-facing failure text (and clear
verificationMessage.value) instead of only setting errorMessage, avoid silently
forcing webhooks.value = [] on network failures (leave existing data or null so
the UI can decide), and ensure verificationError is cleared before retry/loading
attempts so the top banner shows the load failure to the user.

Comment on lines +18 to +124
/v1/webhooks:
post:
tags:
- Webhooks
summary: Create a webhook
operationId: createWebhook
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookCreateRequest'
responses:
"201":
description: Webhook created
content:
application/json:
schema:
$ref: '#/components/schemas/Webhook'
"401":
$ref: '#/components/responses/UnauthorizedResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- bearerAuth: []
get:
tags:
- Webhooks
summary: List webhooks
operationId: listWebhooks
responses:
"200":
description: A collection of webhooks
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Webhook'
"401":
$ref: '#/components/responses/UnauthorizedResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- bearerAuth: []
/v1/webhooks/{webhookId}:
parameters:
- $ref: '#/components/parameters/WebhookId'
get:
tags:
- Webhooks
summary: Retrieve a webhook
operationId: getWebhook
responses:
"200":
description: Webhook details
content:
application/json:
schema:
$ref: '#/components/schemas/Webhook'
"401":
$ref: '#/components/responses/UnauthorizedResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- bearerAuth: []
delete:
tags:
- Webhooks
summary: Delete a webhook
operationId: deleteWebhook
responses:
"204":
description: Webhook deleted
"401":
$ref: '#/components/responses/UnauthorizedResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- bearerAuth: []
/v1/webhooks/{webhookId}/test:
parameters:
- $ref: '#/components/parameters/WebhookId'
post:
tags:
- Webhooks
summary: Test a webhook
operationId: testWebhook
responses:
"200":
description: Webhook tested
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
$ref: '#/components/responses/UnauthorizedResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- bearerAuth: []
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Define missing Webhook components and align listWebhooks response shape

In this OpenAPI file, the new webhook paths currently reference components that don’t exist in components:

  • #/components/schemas/WebhookCreateRequest
  • #/components/schemas/Webhook
  • #/components/parameters/WebhookId

But under components.parameters and components.schemas there are no corresponding WebhookId, WebhookCreateRequest, or Webhook entries. That makes the spec invalid for generators and tooling and should be fixed before publishing.

Additionally, GET /v1/webhooks is documented as returning:

  description: A collection of webhooks
  content:
    application/json:
      schema:
        type: object
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Webhook'

while the router implementation at api/routes/v1/webhooks/index.ts responds with res.json(webhooks) (a bare array). Either the route should wrap the array in { data: webhooks }, or the OpenAPI response schema should be updated to type: array for consistency. The front-end already defensively supports both, but the public spec should match actual HTTP responses.

A minimal patch to make the Webhook references valid could look like:

 components:
   parameters:
+    WebhookId:
+      name: webhookId
+      in: path
+      required: true
+      description: Unique identifier of the webhook.
+      schema:
+        type: string
@@
   schemas:
+    WebhookCreateRequest:
+      type: object
+      required:
+        - url
+        - events
+      properties:
+        url:
+          type: string
+          format: uri
+          description: Fully qualified HTTPS endpoint to receive webhook events.
+        events:
+          type: array
+          description: List of events this webhook should receive.
+          items:
+            type: string
+          minItems: 1
+
+    Webhook:
+      type: object
+      description: Representation of a webhook.
+      properties:
+        id:
+          type: string
+        organizationId:
+          type: string
+        url:
+          type: string
+          format: uri
+        events:
+          type: array
+          items:
+            type: string
+        createdAt:
+          type: string
+          format: date-time
+        updatedAt:
+          type: string
+          format: date-time
+      additionalProperties: true

(Names/fields should be adjusted if your actual Webhook model differs.)

Separately, Checkov’s CKV_OPENAPI_21 about array maxItems is a low-priority hardening suggestion; if you want stricter specs, you can add maxItems to data arrays (e.g., for pagination), but it’s not a functional blocker.

Also applies to: 526-750

🧰 Tools
🪛 Checkov (3.2.334)

[medium] 57-61: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🤖 Prompt for AI Agents
In node-sdk/openapi.yaml around lines 18-124 (and also apply similar fixes to
526-750), the new webhook paths reference missing components
(#/components/schemas/WebhookCreateRequest, #/components/schemas/Webhook,
#/components/parameters/WebhookId) and the GET /v1/webhooks response shape is
inconsistent with the implementation; add a components.parameters.WebhookId
entry and components.schemas.WebhookCreateRequest and components.schemas.Webhook
definitions that match your server model, then either update the GET
/v1/webhooks response schema to return type: array (items: $ref:
'#/components/schemas/Webhook') to match res.json(webhooks) or change the route
to return { data: webhooks } to match the current spec; optionally add maxItems
on array schemas per Checkov CKV_OPENAPI_21 if you want stricter validation.

Copy link

@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: 1

🧹 Nitpick comments (1)
api/routes/v1/domains/index.ts (1)

117-119: Extract duplicated DNS record name logic to improve maintainability.

The condition and transformation for determining DNS record names is duplicated across MX, TXT, and DMARC records. Extracting this to a helper variable would make the code more maintainable and reduce the risk of inconsistencies if the logic needs to change.

Apply this diff to extract the common logic:

+    const recordNamePrefix = domain.name.split('.').length > 2
+      ? domain.name.replace(/\.[^.]+\.[^.]+$/, '')
+      : "@";
+
     return res.json([
       {
         type: "MX",
-        name: domain.name.includes('.') && domain.name.split('.').length > 2
-          ? domain.name.replace(/\.[^.]+\.[^.]+$/, '')
-          : "@",
+        name: recordNamePrefix,
         value: "inbound-smtp.us-east-2.amazonaws.com",
       },
       {
         type: "TXT",
-        name: domain.name.includes('.') && domain.name.split('.').length > 2
-          ? domain.name.replace(/\.[^.]+\.[^.]+$/, '')
-          : "@",
+        name: recordNamePrefix,
         value: "v=spf1 include:amazonses.com ~all",
       },
       {
         type: "TXT",
-        name: `${domain.name.includes('.') && domain.name.split('.').length > 2
-        ? `_dmarc.${domain.name.replace(/\.[^.]+\.[^.]+$/, '')}`
-        : "_dmarc"}`,
+        name: recordNamePrefix === "@" ? "_dmarc" : `_dmarc.${recordNamePrefix}`,
         value: "v=DMARC1; p=reject;",
       },

Note: The redundant includes('.') check has also been removed since split('.').length > 2 already implies the presence of dots.

Also applies to: 124-126, 131-133

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8761430 and 0c7f12d.

📒 Files selected for processing (1)
  • api/routes/v1/domains/index.ts (1 hunks)

Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
api/controllers/WebhookAttemptController.ts (1)

47-59: Align stored payload with sent payload for consistency.

The payload stored in webhookAttempt.payload (lines 47-50) wraps only event and payload, but the actual HTTP request (lines 54-59) sends event, inboxId, messageId, and payload as separate fields. This inconsistency makes it difficult to replay or debug failed webhook attempts, as the stored payload doesn't match what was actually sent.

Apply this diff to store the complete payload that was sent:

- webhookAttempt.payload = {
-   event,
-   payload,
- };
+ const requestPayload = {
+   event,
+   inboxId,
+   messageId,
+   payload,
+ };
+ webhookAttempt.payload = requestPayload;
  webhookAttempt.timestamp = new Date();

  try {
-   const response = await axios.post(webhook.url, {
-     event,
-     inboxId,
-     messageId,
-     payload,
-   });
+   const response = await axios.post(webhook.url, requestPayload);
    webhookAttempt.status = response.status;
    webhookAttempt.response = response.data;
♻️ Duplicate comments (1)
app/app/pages/webhooks.vue (1)

388-391: Surface load failures to the user instead of silently showing an empty state.

The loadWebhooks error handling still stores errors in errorMessage.value, which is only rendered inside the "Add webhook" dialog (line 109). When the initial fetch fails, users see the empty state with no visible error at the page level, even though verificationError is rendered in the banner (lines 24-26).

Based on past review comments, apply this diff:

  } catch (error) {
    console.error('Failed to load webhooks', error);
-   errorMessage.value = 'Unable to load webhooks. Please try again.';
+   verificationError.value = 'Unable to load webhooks. Please try again.';
+   verificationMessage.value = '';
    webhooks.value = [];
  } finally {
🧹 Nitpick comments (2)
api/controllers/WebhookAttemptController.ts (2)

32-35: Consider using a Map for more efficient deduplication.

The current deduplication approach using filter with findIndex has O(n²) complexity. For better performance with larger webhook lists, consider using a Map.

Apply this diff for a more efficient implementation:

- const webhooksWithoutDuplicates = webhooks.filter(
-   (webhook, index, self) =>
-     index === self.findIndex((t) => t.url === webhook.url)
- );
+ const webhooksWithoutDuplicates = Array.from(
+   new Map(webhooks.map(webhook => [webhook.url, webhook])).values()
+ );

72-85: Consider making the limit configurable.

The hard-coded limit of 10 attempts works for the current use case, but making it configurable would improve flexibility for future requirements.

Apply this diff to add an optional limit parameter:

 export async function getWebhookAttemptsByOrganizationIdAndWebhookId({
   organizationId,
   webhookId,
+  limit = 10,
 }: {
   organizationId: string;
   webhookId: string;
+  limit?: number;
 }) {
   return await WebhookAttempt.find({
     organizationId: new mongoose.Types.ObjectId(organizationId),
     webhookId: new mongoose.Types.ObjectId(webhookId),
   })
     .sort({ timestamp: -1 })
-    .limit(10);
+    .limit(limit);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fcfced and 776bc4a.

📒 Files selected for processing (6)
  • README.md (3 hunks)
  • api/controllers/WebhookAttemptController.ts (4 hunks)
  • api/routes/v1/webhooks/attempts.ts (1 hunks)
  • api/routes/v1/webhooks/index.ts (1 hunks)
  • api/routes/webhooks/test.ts (1 hunks)
  • app/app/pages/webhooks.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/routes/v1/webhooks/index.ts
  • api/routes/webhooks/test.ts
🧰 Additional context used
🧬 Code graph analysis (2)
api/routes/v1/webhooks/attempts.ts (1)
api/controllers/WebhookAttemptController.ts (1)
  • getWebhookAttemptsByOrganizationIdAndWebhookId (72-85)
api/controllers/WebhookAttemptController.ts (3)
api/models/Inbox.ts (1)
  • Inbox (3-11)
api/models/Message.ts (1)
  • Message (12-36)
api/models/WebhookAttempt.ts (1)
  • WebhookAttempt (3-16)
🪛 Gitleaks (8.29.0)
README.md

[high] 42-44: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)


[high] 56-58: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

🔇 Additional comments (11)
README.md (3)

42-64: Previous issues resolved: endpoint path and JSON formatting corrected.

The cURL examples now correctly reference the actual API endpoints (/v1/inboxes/{inbox_id}/messages/send and /v1/webhooks) and use valid JSON (no trailing commas). The webhook creation example is a helpful addition that complements the existing inbox and message examples.


68-95: SDK examples are consistent with cURL documentation and correctly updated.

The TypeScript SDK examples align well with the cURL examples:

  • Import from @sendook/node (updated)
  • Field names (text, html, events) match across both interfaces
  • Webhook creation method (client.webhook.create()) follows SDK conventions

42-44: Gitleaks warnings: false positives from placeholder credentials in documentation.

Static analysis flagged authorization headers in the curl examples. However, both use placeholder credentials (your_api_key), which is standard practice in public documentation. These are not actual secrets and do not represent a security concern.

If your organization's security scanning policy requires no literal "Bearer" patterns in documentation, you could use a comment or restructure the example (e.g., -H "Authorization: Bearer $(cat ~/.sendook/api-key)"), but this is optional and not a blocker.

Also applies to: 56-58

api/controllers/WebhookAttemptController.ts (1)

64-65: Good improvement to error handling.

The type-safe error handling now properly checks if the error is an Error instance before accessing .message, preventing potential runtime errors with unknown error types.

app/app/pages/webhooks.vue (7)

450-453: Good validation logic.

The client-side validation correctly checks that at least one event is selected before allowing webhook creation, and the UI reinforces this by disabling the submit button when form.events.length === 0.


528-569: Robust error handling and state management for attempts loading.

The loadAttempts function properly handles all error cases, sets appropriate error messages, and resets the attempts array on failure. The template correctly displays loading, error, empty, and data states.


571-614: Well-implemented test webhook functionality.

The test handler properly clears previous messages, handles errors gracefully, and reloads attempts after a successful test to show the user the immediate result. This provides excellent user feedback.


24-24: Good accessibility implementation throughout.

The page properly uses ARIA attributes including role="alert" for error messages, role="dialog" with aria-modal="true" for dialogs, and aria-label attributes for icon buttons, ensuring screen reader compatibility.

Also applies to: 80-80, 130-130, 139-139, 159-159, 190-190, 224-224, 228-228

Also applies to: 44-44, 76-76, 109-109


317-347: Robust date formatting with appropriate fallbacks.

The formatting functions properly handle missing or invalid date values, returning context-appropriate fallbacks ('recently' for dates, 'N/A' for timestamps) and checking for NaN after Date construction.


616-624: Proper reactive data loading on session changes.

The watcher correctly monitors both organizationId and token, loads webhooks when both are available, and uses immediate: true to trigger the initial load on mount.


1204-1227: Thoughtful responsive design for mobile devices.

The media query at 780px properly adapts the layout for smaller screens, converting flex layouts to columns, adjusting dialog widths, and reducing table font sizes for better mobile usability.

Comment on lines +7 to +22
router.get(
"/",
async (
req: Request<
{ organizationId: string; webhookId: string }
>,
res: Response
) => {
const webhookAttempts = await getWebhookAttemptsByOrganizationIdAndWebhookId({
organizationId: req.organization._id.toString(),
webhookId: req.params.webhookId,
});

return res.json(webhookAttempts);
}
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add error handling and validation for the route handler.

The async handler lacks error handling, which could result in unhandled promise rejections. Additionally, req.organization is accessed without type checking, and webhookId is not validated before querying the database.

Apply this diff to add proper error handling:

 router.get(
   "/",
   async (
     req: Request<
       { organizationId: string; webhookId: string }
     >,
     res: Response
   ) => {
+    try {
+      if (!req.organization?._id) {
+        return res.status(401).json({ error: 'Unauthorized' });
+      }
+
+      if (!req.params.webhookId) {
+        return res.status(400).json({ error: 'Webhook ID is required' });
+      }
+
       const webhookAttempts = await getWebhookAttemptsByOrganizationIdAndWebhookId({
         organizationId: req.organization._id.toString(),
         webhookId: req.params.webhookId,
       });
 
       return res.json(webhookAttempts);
+    } catch (error) {
+      console.error('Failed to fetch webhook attempts:', error);
+      return res.status(500).json({ 
+        error: 'Failed to fetch webhook attempts',
+        message: error instanceof Error ? error.message : 'Unknown error'
+      });
+    }
   }
 );
📝 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
router.get(
"/",
async (
req: Request<
{ organizationId: string; webhookId: string }
>,
res: Response
) => {
const webhookAttempts = await getWebhookAttemptsByOrganizationIdAndWebhookId({
organizationId: req.organization._id.toString(),
webhookId: req.params.webhookId,
});
return res.json(webhookAttempts);
}
);
router.get(
"/",
async (
req: Request<
{ organizationId: string; webhookId: string }
>,
res: Response
) => {
try {
if (!req.organization?._id) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (!req.params.webhookId) {
return res.status(400).json({ error: 'Webhook ID is required' });
}
const webhookAttempts = await getWebhookAttemptsByOrganizationIdAndWebhookId({
organizationId: req.organization._id.toString(),
webhookId: req.params.webhookId,
});
return res.json(webhookAttempts);
} catch (error) {
console.error('Failed to fetch webhook attempts:', error);
return res.status(500).json({
error: 'Failed to fetch webhook attempts',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
);
🤖 Prompt for AI Agents
In api/routes/v1/webhooks/attempts.ts around lines 7 to 22, the GET handler must
validate inputs and handle errors: first check that req.organization exists and
has a valid _id and that req.params.webhookId is present and well-formed (e.g.
non-empty or matches expected ID pattern); if validation fails send a 400
response. Wrap the async call to getWebhookAttemptsByOrganizationIdAndWebhookId
in a try-catch, log the caught error, and return a 500 response on unexpected
failures. Ensure you return after sending responses to avoid double-sends and
keep the handler async/await flow consistent.

@ahmedmawiri ahmedmawiri merged commit 4daeea0 into main Nov 18, 2025
4 checks passed
@ahmedmawiri ahmedmawiri deleted the marc/rup-898-update-readme-for-sendreceive branch November 18, 2025 16:43
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.

3 participants