Skip to content

Feat/stripe webhook#1888

Open
solasamuel wants to merge 11 commits into
honojs:mainfrom
solasamuel:feat/stripe-webhook
Open

Feat/stripe webhook#1888
solasamuel wants to merge 11 commits into
honojs:mainfrom
solasamuel:feat/stripe-webhook

Conversation

@solasamuel
Copy link
Copy Markdown

Summary

Adds @hono/stripe-webhook — Stripe webhook signature verification middleware for Hono. The middleware reads the raw request body, validates the stripe-signature header, and exposes the verified Stripe.Event on the request context for downstream handlers.

Motivation

Webhook signature verification is required by every Hono app handling Stripe payments. There is no official @hono middleware for this today, so every project rolls its own — which is the kind of thing that's easy to get subtly wrong (consuming the body too early, missing the timestamp tolerance, forgetting that constructEvent doesn't run on Workers).

Usage

import { Hono } from 'hono'
import { stripeWebhook } from '@hono/stripe-webhook'

const app = new Hono()

app.post(
  '/webhook',
  stripeWebhook({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  (c) => {
    const event = c.get('stripeEvent')
    // handle event.type ...
    return c.json({ received: true })
  }
)

ContextVariableMap is augmented, so c.get('stripeEvent') is typed without needing a generic on Hono<...>.

API

Option Type Default Description
secret string Required. Endpoint signing secret (whsec_...).
tolerance number 300 Maximum age (seconds) of the signed timestamp before reject.

On verification failure or a missing stripe-signature header, the middleware short-circuits with 400 { error: "Invalid webhook signature" }.

Notes

  • Body handling. Uses c.req.raw.clone().text() so the request stream stays readable for downstream handlers.
  • Async verify. Uses stripe.webhooks.constructEventAsync rather than the sync constructEvent. The sync variant relies on Node's crypto module and won't run on Cloudflare Workers or other edge runtimes; the async variant uses Web Crypto and works everywhere. Same runtime targets as @hono/sentry.
  • Peer dependencies. stripe ^17.0.0 is declared as a peer dep so consumers control the SDK version.
  • Default tolerance. 300 seconds matches Stripe's recommended default.

Testing

vitest suite in src/index.test.ts mocks stripe via vi.mock('stripe', ...) and covers:

  • Valid signature → calls next() and exposes the event on context
  • Missing stripe-signature header → 400
  • Signature verification failure → 400
  • Timestamp outside tolerance window → 400
  • Custom tolerance is forwarded to constructEventAsync
  • Original request body is still readable by downstream handlers after the middleware runs

Checklist

  • Tests pass (yarn test — 6/6)
  • Build succeeds (yarn build)
  • Changeset added
  • README written

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 9, 2026

🦋 Changeset detected

Latest commit: 45ccb3a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hono/stripe-webhook Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/stripe-webhook/src/index.ts Outdated

export const stripeWebhook = (options: Options): MiddlewareHandler => {
const { secret, tolerance = 300 } = options
const stripe = new Stripe(secret, { apiVersion: '2025-02-24.acacia' })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should apiVersion be an option? What other options does the Stripe constructor accept? Should they be exposed too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added apiVersion?: Stripe.LatestApiVersion to Options

Comment thread packages/stripe-webhook/src/index.ts Outdated
Comment on lines +24 to +29
const rawBody = await c.req.raw.clone().text()
const signature = c.req.header('stripe-signature')

if (!signature) {
return c.json({ error: 'Invalid webhook signature' }, 400)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the body be cloned after the signature is checked?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, reordered

Comment thread packages/stripe-webhook/src/index.ts Outdated
Comment on lines +8 to +12
declare module 'hono' {
interface ContextVariableMap {
stripeEvent: Stripe.Event
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally it's better to leave this up to applications to define

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the declare module 'hono' block.

Comment thread packages/stripe-webhook/deno.json Outdated
Comment on lines +2 to +3
"name": "@hono/sentry",
"version": "1.2.2",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs updating

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deno.json name/version — fixed, was a leftover from copying the sentry package.

Sola Samuel and others added 4 commits May 10, 2026 12:23
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yusukebe
Copy link
Copy Markdown
Member

@solasamuel @BarryThePenguin Thanks!

It's hard to accept this PR. We don't add more middleware easily to reduce the maintenance cost. This Stripe webhook feature is simple code and can be implemented by yourself.

It's better to add the instruction for the Stripe webhook on the Examples of our website: https://hono.dev/examples/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants