Skip to content

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 uses the stripe-signature header and signs timestamp.rawBody. Layeron configures that preset for webhooks.stripe(...):

Terminal window
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.

Use webhooks.custom(...) for HMAC signatures:

Terminal window
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:

AlgorithmUse for
hmac-sha256Most modern providers
hmac-sha1Older providers
hmac-sha512Providers that require SHA-512
noneUnsigned internal integrations

Supported encodings are hex, base64, and base64url.

Choose the provider’s signed payload shape:

ValuePayload that gets signed
rawBodyRaw request body
timestamp.rawBodytimestamp.rawBody
method.path.rawBodyMETHOD.path.rawBody

Example:

Terminal window
signature: {
header: "x-vendor-signature",
timestampHeader: "x-vendor-timestamp",
toleranceSeconds: 300,
signedPayload: "timestamp.rawBody",
secret: secret("VENDOR_WEBHOOK_SECRET"),
}

Some providers put several values in one header. Use extract to pull out the actual signature:

Terminal window
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:

Terminal window
signature: {
header: "x-provider-signature",
secret: secret("PROVIDER_WEBHOOK_SECRET"),
extract: /v1=([a-f0-9]+)/,
}

Use verify when the provider uses a special format:

Terminal window
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 follow Layeron’s platform namespace defaults:

Terminal window
secret("VENDOR_WEBHOOK_SECRET")

Use an explicit namespace when teams or environments share a secret name:

Terminal window
secret("WEBHOOK_SECRET", { namespace: "billing" })

Use the same secret reference in outbound signing:

Terminal window
signing: {
header: "x-layeron-signature",
secret: secret("CRM_WEBHOOK_SIGNING_SECRET"),
}

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.

Terminal window
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:

Terminal window
const webhookSecret = await customerEvents.secrets.get({
userId: "user_123",
})
console.log(webhookSecret.current.value)

The value looks like:

Terminal window
whcms_bDg2Y2...

Rotate it when a customer asks for a new secret:

Terminal window
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.