Skip to content

Webhooks

CruzJS provides an outbound webhook system for notifying external services when events occur in your application. Webhooks are org-scoped, HMAC-signed, and include delivery logging with automatic retries.

Register the WebhookModule in your application:

import { WebhookModule } from '@cruzjs/core/webhooks';
export default createCruzApp({
modules: [WebhookModule],
});

Each webhook has an endpoint URL, a signing secret, and a list of events it subscribes to:

trpc.webhook.create.useMutation().mutate({
url: 'https://example.com/webhooks/cruzjs',
events: ['invoice.created', 'invoice.paid'],
});

The server generates a signing secret automatically and returns it in the response. Store this secret on the receiving end to verify webhook signatures.

Use WebhookService.dispatch() to send a webhook payload to all matching endpoints:

import { Injectable, Inject } from '@cruzjs/core/di';
import { WebhookService } from '@cruzjs/core/webhooks';
@Injectable()
export class InvoiceService {
constructor(
@Inject(WebhookService) private readonly webhooks: WebhookService,
) {}
async createInvoice(orgId: string, input: CreateInvoiceInput) {
const invoice = await this.saveInvoice(input);
await this.webhooks.dispatch('invoice.created', {
id: invoice.id,
amount: invoice.amount,
currency: invoice.currency,
createdAt: invoice.createdAt,
}, orgId);
return invoice;
}
}

The dispatch method finds all webhooks in the org that subscribe to the given event name and sends an HTTP POST to each endpoint.

Every webhook request includes an X-Cruz-Signature header containing an HMAC-SHA256 signature of the request body, signed with the webhook’s secret:

X-Cruz-Signature: sha256=5d41402abc4b2a76b9719d911017c592
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(
body: string,
signature: string,
secret: string,
): boolean {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(body)
.digest('hex');
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}

Every webhook dispatch is logged with the response status, response body, and timestamp. View delivery history via tRPC:

const { data: deliveries } = trpc.webhook.deliveries.useQuery({
webhookId: 'wh_abc123',
});
// deliveries: [{ id, status, responseCode, responseBody, createdAt }, ...]

Failed deliveries (non-2xx responses or network errors) are retried with exponential backoff:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours

After all retries are exhausted, the delivery is marked as failed in the delivery log.

Send a test payload to verify your endpoint is configured correctly:

trpc.webhook.test.useMutation().mutate({
webhookId: 'wh_abc123',
});

This sends a webhook.test event with a sample payload and returns the response status.

All procedures are org-scoped.

ProcedureTypeDescription
webhook.listqueryList all webhooks for the current org
webhook.createmutationCreate a new webhook endpoint
webhook.updatemutationUpdate URL or subscribed events
webhook.deletemutationDelete a webhook
webhook.testmutationSend a test event to a webhook
webhook.deliveriesqueryView delivery logs for a webhook

Combine webhooks with domain events to automatically notify external systems:

import { Module } from '@cruzjs/core/di';
import { InvoiceCreatedEvent } from './events';
import { getAppContainer } from '@cruzjs/core';
import { WebhookService } from '@cruzjs/core/webhooks';
async function dispatchInvoiceWebhook(event: InvoiceCreatedEvent) {
const container = await getAppContainer();
const webhooks = container.resolve(WebhookService);
await webhooks.dispatch('invoice.created', {
invoiceId: event.invoiceId,
amount: event.amount,
currency: event.currency,
}, event.orgId);
}
@Module({
events: [
{ event: InvoiceCreatedEvent, listener: dispatchInvoiceWebhook },
],
})
export class InvoiceWebhookModule {}