Skip to content

Webhooks Domain

The webhooks domain provides outbound HTTP event delivery with URL validation, SSRF protection, and delivery tracking for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.

Terminal window
npx @backcap/cli add webhooks

The Webhook entity is the aggregate root. It tracks subscribed events, delivery secret, and active state through guarded mutations.

import { Webhook } from "./domains/webhooks/domain/entities/webhook.entity";
const result = Webhook.create({
id: crypto.randomUUID(),
url: "https://example.com/hook",
events: ["user.created", "order.completed"],
secret: "whsec_abc123",
});
if (result.isOk()) {
const webhook = result.unwrap();
console.log(webhook.id, webhook.url.value, webhook.isActive); // true
webhook.deactivate();
console.log(webhook.isActive); // false
}
FieldTypeDescription
idstringUnique identifier (UUID)
urlWebhookUrlValidated URL value object (rejects private IPs)
eventsstring[]Event types this webhook subscribes to
secretstringHMAC secret for payload signing
isActivebooleanWhether the webhook is active
createdAtDateTimestamp of creation

Webhook.create() returns Result<Webhook, InvalidWebhookUrl | Error>. Empty events arrays are rejected.

State transitions:

  • activate() — marks the webhook as active (fails if already active)
  • deactivate() — marks the webhook as inactive (fails if already inactive)
import { WebhookUrl } from "./domains/webhooks/domain/value-objects/webhook-url.vo";
const result = WebhookUrl.create("https://example.com/hook");
// Result<WebhookUrl, InvalidWebhookUrl>
if (result.isOk()) {
const url = result.unwrap();
console.log(url.value); // "https://example.com/hook"
}

Validates: must be a valid HTTPS/HTTP URL. Rejects private and reserved IPs (127.x.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 0.0.0.0, 169.254.x.x, ::1, fe80:, fc00:, fd). Pass { allowPrivate: true } to bypass SSRF checks in development.

Error ClassConditionMessage
WebhookNotFoundNo webhook found for the given IDWebhook not found with id: "<id>"
InvalidWebhookUrlURL fails format or SSRF validationInvalid webhook URL: "<url>"
WebhookDeliveryFailedDelivery returned a non-success statusWebhook delivery failed: "<reason>"
EventEmitted ByPayload
WebhookDeliveredTriggerWebhook use casewebhookId, eventType, statusCode, occurredAt
WebhookFailedTriggerWebhook use casewebhookId, eventType, reason, occurredAt

Creates a new webhook entity, validates the URL and events, and persists it.

import { RegisterWebhook } from "./domains/webhooks/application/use-cases/register-webhook.use-case";
const registerWebhook = new RegisterWebhook(webhookRepository, webhookDelivery);
const result = await registerWebhook.execute({
url: "https://example.com/hook",
events: ["user.created"],
secret: "whsec_abc123",
});
// Result<{ webhookId: string; createdAt: Date }, Error>

Possible failures: InvalidWebhookUrl, empty events

Delivers a payload to a webhook endpoint. Only delivers if the webhook subscribes to the given event type.

import { TriggerWebhook } from "./domains/webhooks/application/use-cases/trigger-webhook.use-case";
const triggerWebhook = new TriggerWebhook(webhookRepository, webhookDelivery);
const result = await triggerWebhook.execute({
webhookId: "wh-123",
eventType: "user.created",
payload: { userId: "u-1", email: "user@example.com" },
});
// Result<{ deliveredAt: Date; statusCode: number; event: WebhookDelivered }, Error>

Possible failures: WebhookNotFound, WebhookDeliveryFailed, event type not subscribed

Lists webhooks with optional filtering and pagination.

import { ListWebhooks } from "./domains/webhooks/application/use-cases/list-webhooks.use-case";
const listWebhooks = new ListWebhooks(webhookRepository, webhookDelivery);
const result = await listWebhooks.execute({ isActive: true, limit: 10, offset: 0 });
// Result<{ webhooks: Array<{ id, url, events, isActive, createdAt }>; total: number }, Error>
export interface IWebhookRepository {
save(webhook: Webhook): Promise<void>;
findById(id: string): Promise<Webhook | undefined>;
findAll(filters: WebhookFilters): Promise<{ webhooks: Webhook[]; total: number }>;
}
export interface IWebhookDelivery {
deliver(
url: string,
secret: string,
eventType: string,
payload: unknown,
): Promise<{ statusCode: number }>;
}

The delivery port dispatches the HTTP request to the webhook URL. It does not persist.

import {
createWebhooksDomain,
IWebhooksService,
} from "./domains/webhooks/contracts";
const webhooksService: IWebhooksService = createWebhooksDomain({
webhookRepository,
webhookDelivery,
});
// IWebhooksService interface:
// register(input): Promise<Result<WebhooksRegisterOutput, Error>>
// trigger(input): Promise<Result<WebhooksTriggerOutput, Error>>
// list(input): Promise<Result<WebhooksListOutput, Error>>

This is the only import consumers need. The internal use case classes are implementation details.

domains/webhooks/
domain/
entities/webhook.entity.ts
value-objects/webhook-url.vo.ts
errors/webhook-not-found.error.ts
errors/invalid-webhook-url.error.ts
errors/webhook-delivery-failed.error.ts
events/webhook-delivered.event.ts
events/webhook-failed.event.ts
application/
use-cases/register-webhook.use-case.ts
use-cases/trigger-webhook.use-case.ts
use-cases/list-webhooks.use-case.ts
ports/webhook-repository.port.ts
ports/webhook-delivery.port.ts
dto/register-webhook.dto.ts
dto/trigger-webhook.dto.ts
dto/list-webhooks.dto.ts
contracts/
webhooks.contract.ts
webhooks.factory.ts
index.ts
shared/
result.ts