---
title: Framework adapters
description: Mount the same handler on Next.js, Hono, Express, bare Node — or anything that speaks Request.
---

The handler's native interface is Web-standard: `handler.fetch` is a
`(request: Request) => Promise<Response>` function. If your platform speaks
`Request` — Next.js App Router, Cloudflare Workers, Deno, Bun, Remix — you
need no adapter at all:

```ts
export const POST = handler.fetch
```

Adapters exist for the platforms that don't, each on its own subpath so
nothing you skip is bundled.

## Next.js App Router — `webhooks-sdk/next`

```ts app/api/webhooks/stripe/route.ts
import { toNextRoute } from 'webhooks-sdk/next'

export const { POST } = toNextRoute(handler)
```

`toNextRoute` also returns `GET` and `PUT` — export those too for providers
that confirm a new endpoint with a challenge query parameter:

```ts
export const { POST, GET } = toNextRoute(handler)
```

For the **Pages Router**, disable the body parser and use the
[Node adapter](#bare-node--webhooks-sdknode):

```ts pages/api/webhooks/stripe.ts
import { toNodeHandler } from 'webhooks-sdk/node'

export const config = { api: { bodyParser: false } }
export default toNodeHandler(handler)
```

## Hono — `webhooks-sdk/hono`

```ts
import { toHonoHandler } from 'webhooks-sdk/hono'

app.post('/webhooks/stripe', toHonoHandler(handler))
```

Works wherever Hono does — Workers, Deno, Bun, Node. The adapter is
structurally typed against the Hono context, so Hono is not a dependency of
the SDK.

## Express — `webhooks-sdk/express`

Express is where the [raw body](/docs/concepts/raw-body) bites. Two ways to get
it right:

**Raw route (preferred)**

Mount the webhook route with a raw body *before* any JSON parser:

```ts
import express from 'express'
import { toExpressHandler } from 'webhooks-sdk/express'

app.post('/webhooks/stripe', express.raw({ type: '*/*' }), toExpressHandler(handler))
app.use(express.json())
```

**captureRawBody**

If you can't reorder middleware, capture the bytes before the global
parser consumes them:

```ts
import { captureRawBody, toExpressHandler } from 'webhooks-sdk/express'

app.use(express.json({ verify: captureRawBody }))
app.post('/webhooks/stripe', toExpressHandler(handler))
```

The adapter picks up the untouched bytes from either path — and handles the
detail that Node pools Buffers, so a captured body is often a view into a
larger allocation.

## Bare Node — `webhooks-sdk/node`

For `node:http` servers and anything built on them:

```ts
import { createServer } from 'node:http'
import { toNodeHandler } from 'webhooks-sdk/node'

const server = createServer(toNodeHandler(handler))
```

The module also exports the building blocks — `readRawBody(request)` and
`fromNodeRequest(request, rawBody?)` — if you're wiring into a framework
with its own request type. Everything is structurally typed, so the SDK
never depends on `@types/node`.

:::warning[Twilio needs an absolute URL on Node]
Twilio signs your endpoint's full public URL, but Node's `req.url` is just a
path. Behind `toNodeHandler` or `toExpressHandler`, configure the
[Twilio provider](/docs/providers/twilio) with an explicit `url` — the SDK throws
a `ConfigurationError` at startup rather than failing every delivery as a
bad signature.
:::

## Response behavior

All adapters produce the same responses `handler.fetch` would: handshake
responses pass through verbatim, errors return their
[status and JSON body](/docs/concepts/errors), everything else returns `200`.
