Send webhooks
Use outbound webhooks when your app needs to notify another system about events such as payment updates, account changes, or workflow milestones.
Configure endpoints
Section titled “Configure endpoints”Create an outbound webhook with webhooks.out(...):
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.
Send an event
Section titled “Send an event”Call send(eventType, payload) from route handlers, jobs, or internal service
code:
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:
{ eventId: "whevt_...", deliveries: [ { endpoint: "crm", status: "queued", attemptId: "whatt_...", }, ],}Sign outbound deliveries
Section titled “Sign outbound deliveries”Outbound signing uses the same signature options as inbound verification:
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.
Give each user a managed secret
Section titled “Give each user a managed secret”For customer-facing webhooks, each user can have a Layeron-managed signing
secret. The value starts with whcms_ and is generated per user:
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:
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:
await customerEvents.send( "customer.updated", { customerId: customer.id, }, { userId: customer.ownerId, },)If you already have the user ID, pass it directly:
await customerEvents.send( "invoice.paid", { invoiceId: invoice.id }, { userId: invoice.ownerId },)Rotate a user’s secret from an admin or settings flow:
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.
Per-endpoint retries
Section titled “Per-endpoint retries”Set endpoint retry behavior when a destination needs a different policy:
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, }, }, ],})Inspect sent events
Section titled “Inspect sent events”Outbound events are listed with direction: "outbound":
const events = await billingEvents.events.list({ direction: "outbound", limit: 50,})Use events.get(eventId) to inspect payload, headers, status, and timestamps.