# @postel/http



`@postel/http` is the framework-neutral layer every adapter is built on. Use it directly on any runtime whose requests are Web `Request`s — Deno, Next.js Route Handlers, Bun, Cloudflare Workers, a bare server — or as the base for your own adapter.

## fetchWebhook [#fetchwebhook]

```ts
import { fetchWebhook } from "@postel/http";
import { postel } from "./postel";

const handler = fetchWebhook(postel.inbound.vendor, {
  onVerified: async ({ event }) => {
    await handleOrder(event);
  },
});

Deno.serve((req) => handler(req)); // Deno
export const POST = (req: Request) => handler(req); // Next.js Route Handler
```

`fetchWebhook(source, opts)` returns `(req: Request) => Promise<Response>`: it reads the raw bytes, runs the verifier(s) you configured, maps `PostelError` → status (`SIGNATURE_INVALID` / `TIMESTAMP_TOO_OLD` / `MALFORMED_HEADER` → 400, `UNKNOWN_KEY_ID` → 401), runs `onVerified`, and returns `204` (or the response your handler returns). A non-`PostelError` is rethrown so the runtime yields 5xx.

## handleInbound [#handleinbound]

For frameworks that aren't Fetch-native, work with the normalized outcome and write your own response:

```ts
import { handleInbound } from "@postel/http";

const outcome = await handleInbound(source, { rawBody, headers, method }, opts);
// outcome.kind: "verified" | "duplicate" | "error"
// + outcome.status, outcome.headers, outcome.body, outcome.context (verified result)
```

`@postel/http/node` adds `writeOutcomeToNodeRes(res, outcome)` and `headersFromNode(req.headers)` for Node `req`/`res` frameworks; the error→status policy is exported as `statusForError` / `errorBody`. (This is exactly what the Express and Fastify adapters use under the hood.)

## Dedup-ack [#dedup-ack]

Pass `dedup: { ttl }` (with a dedup adapter configured on the source) and a repeated `webhook-id` is acknowledged `2xx` with `X-Postel-Dedup-Result: duplicate` — checked **after** verification, so an unauthenticated id can never short-circuit handling.

## releaseOnThrow [#releaseonthrow]

`releaseOnThrow(source, outcome, fn)` runs a gate's downstream handler and, when it throws after the request wrote a fresh dedup record (`outcome.dedupRecorded`), releases that record before the error propagates. Every framework adapter's handler-wrapping path is built on it; use it when composing your own gate over `handleInbound`.

## JWKS [#jwks]

`jwksFetchHandler(provider)` returns a Fetch handler that serves your current public keys per request — the `JwksProvider` is any `() => Awaitable<Jwks>`, typically `() => postel.outbound.keys.publicJwks()`. See [key rotation](/docs/inbound/key-rotation).

## Exports [#exports]

| Export                                            | What it is                                                                                                                                         |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fetchWebhook(source, opts?)`                     | Fetch-native gate: `(req: Request) => Promise<Response>`.                                                                                          |
| `handleInbound(source, req, opts?)`               | The core gate — returns a `WebhookOutcome` instead of writing a response.                                                                          |
| `releaseOnThrow(source, outcome, fn)`             | Runs a downstream handler; releases a freshly-written dedup record on throw.                                                                       |
| `jwksFetchHandler(provider)`                      | Fetch handler serving a `JwksProvider`'s keys.                                                                                                     |
| `statusForError(err)` / `errorBody(err)`          | The [code→status mapping](/docs/reference/errors#http-status-mapping) and the JSON error body.                                                     |
| `WebhookOutcome<TData>`                           | `{ kind: "verified" \| "duplicate" \| "error" }` union with `status`/`headers`/`body`; the verified variant carries `context` and `dedupRecorded`. |
| `WebhookContext<TData>`                           | What a verified request hands your handler: `result`, `event`, `messageId`, `headers`, `rawBody`.                                                  |
| `WebhookHandlerOptions<TData>`                    | Gate options: `onVerified`, `successStatus`, `dedup` (`DedupAckOptions`: `{ ttl, duplicateStatus? }`).                                             |
| `GateSource<TData>`                               | The structural source a gate accepts — `verify` plus optional `dedup`/`dedupRelease`; `postel.inbound.<source>` satisfies it.                      |
| `NormalizedRequest` / `RawBody` / `WebhookMethod` | The framework-neutral request shape (`rawBody`, `headers`, `method`), its body type, and the gateable methods (`POST` \| `PUT` \| `PATCH`).        |
| `HandlerResponse` / `HandlerResponseInit`         | What `onVerified` may return to override the success response.                                                                                     |
| `JwksProvider`                                    | `() => Awaitable<Jwks>` for `jwksFetchHandler`.                                                                                                    |

From `@postel/http/node`: `headersFromNode(headers)` (flattens Node's `string | string[]` values), `writeOutcomeToNodeRes(res, outcome)`, `writeResponseToNodeRes(res, response)`, and the `NodeResponseLike` shape they write to.
