@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.

View as Markdown

@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 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

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

ExportWhat 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 / WebhookMethodThe framework-neutral request shape (rawBody, headers, method), its body type, and the gateable methods (POST | PUT | PATCH).
HandlerResponse / HandlerResponseInitWhat 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.

On this page