Skip to main content
When your function receives webhooks from an external service, anyone who knows the URL can send it fake data. Signature verification confirms the event really came from the service. ctx.verify* does that check in one line: it reads the signature header, resolves the secret from your secrets, and throws if it does not match.

Stripe

index.ts
Set the secret once:
The event your handler receives is already the Zod-validated body. The platform consumed the request body to validate it, so calling request.json() inside the handler throws — use the first parameter.

Supported providers

Stripe additionally rejects events older than 5 minutes, which stops someone from replaying a captured event.

Other services

For Twilio, GitHub, Slack or any service with an HMAC-SHA256 signature, use ctx.verifyHmac. The header is required because there is no default:

Rotate secrets

During a rotation, accept both the old and the new secret at once by comma-separating them:
Once you confirm the provider is using the new one, go back to a single value.

Handle the failure

Verifiers throw WebhookVerificationError with a code telling you what happened:
If you do not catch the error, the function responds with an error and the provider will retry the webhook. For Stripe and Shopify that is usually right only when the failure is transient; on an invalid signature it is better to return 401 and not retry.

Public functions

Webhooks need config.public: true so the external service can call them without Jelou credentials. That is exactly why signature verification is mandatory: it is the only access control left.
See public functions.

Testing

In tests, createMockContext() leaves every verifier throwing missing_secret. To exercise the rest of the handler, bypass verification:
See the testing guide.

Public functions

Receive requests without Jelou credentials.

Secrets

Store each provider’s secret.

Deferred runs

Book a follow-up when the webhook arrives.

Webhook receiver

Full copy-paste example.