Signatures and Secrets
Use signatures to prove a webhook came from the provider you expect. Use Secrets for verification keys and outbound signing keys.
Stripe signatures
Section titled “Stripe signatures”Stripe uses the stripe-signature header and signs timestamp.rawBody.
Layeron configures that preset for webhooks.stripe(...):
const stripe = webhooks.stripe({ path: "/webhooks/stripe", secret: secret("STRIPE_WEBHOOK_SECRET"), handler: async (event) => { await processStripeEvent(event.payload) },})The default Stripe tolerance is 300 seconds.
Standard HMAC signatures
Section titled “Standard HMAC signatures”Use webhooks.custom(...) for HMAC signatures:
const vendor = webhooks.custom({ name: "vendor", path: "/webhooks/vendor", signature: { header: "x-signature", algorithm: "hmac-sha256", encoding: "hex", secret: secret("VENDOR_WEBHOOK_SECRET"), signedPayload: "rawBody", }, handler: async (event) => { await processVendorEvent(event.payload) },})Supported algorithms:
| Algorithm | Use for |
|---|---|
hmac-sha256 | Most modern providers |
hmac-sha1 | Older providers |
hmac-sha512 | Providers that require SHA-512 |
none | Unsigned internal integrations |
Supported encodings are hex, base64, and base64url.
Signed payloads
Section titled “Signed payloads”Choose the provider’s signed payload shape:
| Value | Payload that gets signed |
|---|---|
rawBody | Raw request body |
timestamp.rawBody | timestamp.rawBody |
method.path.rawBody | METHOD.path.rawBody |
Example:
signature: { header: "x-vendor-signature", timestampHeader: "x-vendor-timestamp", toleranceSeconds: 300, signedPayload: "timestamp.rawBody", secret: secret("VENDOR_WEBHOOK_SECRET"),}Extract unusual signatures
Section titled “Extract unusual signatures”Some providers put several values in one header. Use extract to pull out the
actual signature:
signature: { header: "x-provider-signature", secret: secret("PROVIDER_WEBHOOK_SECRET"), extract: (ctx) => { const header = ctx.headers.get("x-provider-signature") ?? "" return header.split(",").find((part) => part.startsWith("v1="))?.slice(3) ?? "" },}You can also pass a regular expression:
signature: { header: "x-provider-signature", secret: secret("PROVIDER_WEBHOOK_SECRET"), extract: /v1=([a-f0-9]+)/,}Fully custom verification
Section titled “Fully custom verification”Use verify when the provider uses a special format:
signature: { secret: secret("CUSTOM_WEBHOOK_SECRET"), verify: async (ctx) => { const signature = ctx.headers.get("x-custom-signature") const expected = await buildProviderSignature({ secret: ctx.secret, method: ctx.method, path: ctx.path, body: ctx.rawBody, })
return signature === expected },}The verification context includes the Request, raw body, headers, method,
path, timestamp, extracted signature, and secret value.
Secret References
Section titled “Secret References”Secret references follow Layeron’s platform namespace defaults:
secret("VENDOR_WEBHOOK_SECRET")Use an explicit namespace when teams or environments share a secret name:
secret("WEBHOOK_SECRET", { namespace: "billing" })Use the same secret reference in outbound signing:
signing: { header: "x-layeron-signature", secret: secret("CRM_WEBHOOK_SIGNING_SECRET"),}Managed customer secrets
Section titled “Managed customer secrets”Outbound webhooks can generate one signing secret per user. This is useful when customers configure their own webhook endpoint and need a secret they can copy into their system.
const customerEvents = webhooks.out({ name: "customer-events", endpoints: [ { name: "customer", url: "https://customer.example.com/hooks", }, ], managedSecrets: { prefix: "whcms", rotation: { retain: 2, }, },})Get the current user’s secret:
const webhookSecret = await customerEvents.secrets.get({ userId: "user_123",})
console.log(webhookSecret.current.value)The value looks like:
whcms_bDg2Y2...Rotate it when a customer asks for a new secret:
await customerEvents.secrets.rotate({ userId: "user_123",})Layeron stores managed customer secrets in encrypted Storage KV. The encryption key is a product-managed random secret.