Skip to content

Send webhooks

Use outbound webhooks when your app needs to notify another system about events such as payment updates, account changes, or workflow milestones.

Create an outbound webhook with webhooks.out(...):

Terminal window
import { backend, secret } from "@layeron/core"
import { webhooks } from "@layeron/modules"
const app = backend()
const billingEvents = webhooks.out({
name: "billing-events",
endpoints: [
{
name: "crm",
url: "https://crm.example.com/hooks/layeron",
signing: {
header: "x-layeron-signature",
algorithm: "hmac-sha256",
encoding: "hex",
secret: secret("CRM_WEBHOOK_SIGNING_SECRET"),
},
},
{
name: "warehouse",
url: "https://warehouse.example.com/events",
},
],
})
app.use(billingEvents)

Each endpoint has its own name, URL, signing rule, and retry options.

Call send(eventType, payload) from route handlers, jobs, or internal service code:

Terminal window
app.post("/billing/invoices/:id/pay", async (request) => {
const pathSegments = new URL(request.url).pathname.split("/")
const id = pathSegments[3]
const invoice = await payInvoice(id)
const result = await billingEvents.send("invoice.paid", {
invoiceId: invoice.id,
customerId: invoice.customerId,
paidAt: invoice.paidAt,
})
return Response.json({
eventId: result.eventId,
deliveries: result.deliveries,
})
})

The result includes one delivery entry per endpoint:

Terminal window
{
eventId: "whevt_...",
deliveries: [
{
endpoint: "crm",
status: "queued",
attemptId: "whatt_...",
},
],
}

Outbound signing uses the same signature options as inbound verification:

Terminal window
signing: {
header: "x-layeron-signature",
timestampHeader: "x-layeron-timestamp",
signedPayload: "timestamp.rawBody",
secret: secret("PARTNER_SIGNING_SECRET"),
}

Use a dedicated signing secret for each external destination when possible.

For customer-facing webhooks, each user can have a Layeron-managed signing secret. The value starts with whcms_ and is generated per user:

Terminal window
const customerEvents = webhooks.out({
name: "customer-events",
endpoints: [
{
name: "customer",
url: "https://customer.example.com/hooks",
},
],
})
app.use(customerEvents)

Expose the current user’s secret from an authenticated settings route:

Terminal window
app.get("/settings/webhooks/secret", async (request) => {
const userId = await requireUserId(request)
const webhookSecret = await customerEvents.secrets.get({
userId,
})
return Response.json({
secret: webhookSecret.current.value,
})
})

Send an event for the same user. Layeron loads the user’s whcms_... secret and signs the delivery:

Terminal window
await customerEvents.send(
"customer.updated",
{
customerId: customer.id,
},
{
userId: customer.ownerId,
},
)

If you already have the user ID, pass it directly:

Terminal window
await customerEvents.send(
"invoice.paid",
{ invoiceId: invoice.id },
{ userId: invoice.ownerId },
)

Rotate a user’s secret from an admin or settings flow:

Terminal window
const rotated = await customerEvents.secrets.rotate({
userId: "user_123",
})

The previous secret is retained so in-flight deliveries can still be verified during a rotation window.

Set endpoint retry behavior when a destination needs a different policy:

Terminal window
const partnerEvents = webhooks.out({
name: "partner-events",
endpoints: [
{
name: "partner",
url: "https://partner.example.com/webhooks",
retry: {
attempts: 8,
backoff: "exponential",
initialDelaySeconds: 10,
maxDelaySeconds: 600,
},
},
],
})

Outbound events are listed with direction: "outbound":

Terminal window
const events = await billingEvents.events.list({
direction: "outbound",
limit: 50,
})

Use events.get(eventId) to inspect payload, headers, status, and timestamps.