Skip to content

Endpoint Management ​

The WebhookEndpointAdminService provides full CRUD for webhook endpoints. Use it to build your webhook management UI or API.

Create an Endpoint ​

typescript
import { BadRequestException, Injectable } from '@nestjs/common';
import { WebhookEndpointAdminService } from '@nestarc/webhook';

function requireHttpsWebhookUrl(value: string): string {
  let parsed: URL;
  try {
    parsed = new URL(value);
  } catch {
    throw new BadRequestException('Webhook URL must be valid');
  }
  if (parsed.protocol !== 'https:') {
    throw new BadRequestException('Webhook URL must use HTTPS');
  }
  return parsed.toString();
}

@Injectable()
export class WebhookController {
  constructor(private readonly endpointAdmin: WebhookEndpointAdminService) {}

  async register(dto: RegisterWebhookDto) {
    const endpoint = await this.endpointAdmin.createEndpoint({
      url: requireHttpsWebhookUrl(dto.url),
      events: ['order.created', 'order.paid'],
      secret: 'auto',              // optional; generates a random 32-byte base64 secret
      description: 'Order events',
      metadata: { team: 'payments' },
      tenantId: dto.tenantId,
    });

    // Store this returned secret; read APIs will not return it later
    return {
      id: endpoint.id,
      secret: endpoint.secret,
    };
  }
}

The package's default SSRF checks block private and internal destinations, but the current release still permits public http: URLs. Enforce HTTPS in every application-owned create, update, and import path before calling WebhookEndpointAdminService.

WARNING

The new signing secret is returned by createEndpoint() and rotateSecret(). Read operations (listEndpoints, getEndpoint) exclude secrets. Store it securely on the customer side.

List Endpoints ​

typescript
// All endpoints
const endpoints = await this.endpointAdmin.listEndpoints();

// Filter by tenant
const tenantEndpoints = await this.endpointAdmin.listEndpoints('tenant_abc');

Returns an array of EndpointRecord (secret excluded).

Get a Single Endpoint ​

typescript
const endpoint = await this.endpointAdmin.getEndpoint('endpoint-uuid');
// Returns EndpointRecord | null

Update an Endpoint ​

typescript
const updated = await this.endpointAdmin.updateEndpoint('endpoint-uuid', {
  url: 'https://new-url.example.com/webhooks',
  events: ['order.created', 'order.paid', 'order.cancelled'],
  description: 'Updated description',
  active: true,
});

All fields in the update DTO are optional — only provided fields are updated. Deactivation prevents new deliveries from being created; it does not cancel existing pending or retrying deliveries. Changing a URL does not alter already queued URL snapshots.

Rotate a Signing Secret ​

Rotate secrets with an overlap window so receivers can accept the previous and new signatures while provisioning completes:

typescript
const rotated = await this.endpointAdmin.rotateSecret('endpoint-uuid', {
  previousSecretExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
});

if (!rotated) {
  throw new NotFoundException('Webhook endpoint not found');
}

// Returned only from this rotation response. Provision it immediately.
await this.receiverSecrets.store(rotated.secret);

Omit secret or pass 'auto' to generate a new 32-byte base64 secret. New deliveries created before previousSecretExpiresAt snapshot both secrets. The expiry is not rechecked during dispatch: deliveries queued before rotation keep the old key, and those queued during overlap can still sign with both keys after expiry. Coordinate receiver key retirement with pending and manually retryable deliveries. Read APIs never return either secret.

Delete an Endpoint ​

typescript
const deleted = await this.endpointAdmin.deleteEndpoint('endpoint-uuid');
// Returns boolean

This is a hard delete. The default PostgreSQL schema keeps delivery history linked to the endpoint, so deletion may be rejected while delivery rows still reference it. Decide whether your admin workflow should deactivate the endpoint, retain its audit history, or purge related records through an explicitly reviewed process.

Send a Test Event ​

typescript
const eventId = await this.endpointAdmin.sendTestEvent('endpoint-uuid');
// Sends a 'webhook.test' event with an empty payload to the endpoint

Use this to let customers verify their endpoint is reachable and correctly configured. The returned event ID means a test was queued, not that its HTTP request succeeded; inspect delivery logs and the receiver result.

WebhookEndpointAdminService API ​

MethodSignatureDescription
createEndpoint(dto: CreateEndpointDto) => Promise<EndpointRecordWithSecret>Register endpoint; returns record with secret
listEndpoints(tenantId?: string) => Promise<EndpointRecord[]>List endpoints (secret excluded)
getEndpoint(id: string) => Promise<EndpointRecord | null>Get single endpoint (secret excluded)
updateEndpoint(id: string, dto: UpdateEndpointDto) => Promise<EndpointRecord | null>Partial update
rotateSecret(id: string, dto: RotateEndpointSecretDto) => Promise<EndpointRecordWithSecret | null>Rotate the signing secret with an overlap window; returns the new secret once
deleteEndpoint(id: string) => Promise<boolean>Hard delete
sendTestEvent(endpointId: string) => Promise<string | null>Send webhook.test ping

Data Types ​

CreateEndpointDto ​

typescript
interface CreateEndpointDto {
  url: string;                              // HTTPS endpoint URL
  events: string[];                         // Event types to subscribe
  secret?: string | 'auto';                 // 'auto' generates 32-byte base64 secret
  description?: string;                     // Optional description
  metadata?: Record<string, unknown>;       // Optional key-value metadata
  tenantId?: string;                        // Optional tenant scope
}

Omitting secret has the same effect as passing the case-sensitive value 'auto'.

UpdateEndpointDto ​

typescript
interface UpdateEndpointDto {
  url?: string;
  events?: string[];
  description?: string;
  metadata?: Record<string, unknown>;
  active?: boolean;
}

RotateEndpointSecretDto ​

typescript
interface RotateEndpointSecretDto {
  secret?: string;                  // valid base64, at least 16 decoded bytes
  previousSecretExpiresAt: Date;    // required future timestamp
}

EndpointRecord ​

typescript
interface EndpointRecord {
  id: string;
  url: string;
  events: string[];
  active: boolean;
  description: string | null;
  metadata: Record<string, unknown> | null;
  tenantId: string | null;
  consecutiveFailures: number;
  disabledAt: Date | null;
  disabledReason: string | null;
  previousSecretExpiresAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
}

TIP

The consecutiveFailures, disabledAt, and disabledReason fields are managed by the circuit breaker. You can expose them in your admin UI to show endpoint health at a glance.

Released under the MIT License.