@postel/http
The framework-agnostic webhook HTTP core — verify, map errors, serve JWKS — usable on any Fetch runtime or as the base for a custom adapter.
@postel/http is the framework-neutral layer every adapter is built on. Use it directly on any runtime whose requests are Web Requests — Deno, Next.js Route Handlers, Bun, Cloudflare Workers, a bare server — or as the base for your own adapter.
fetchWebhook
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 HandlerfetchWebhook(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
For frameworks that aren't Fetch-native, work with the normalized outcome and write your own response:
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
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(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
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.
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 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.