Web adapters

Hono

Verify webhooks in Hono with HonoWebAdapter — a routing facade that gates each source, publishes JWKS, and mounts admin.

lib/postel.ts
import { Postel, Secret } from "@postel/core";
import { config } from "./config.js";

export const postel = Postel({
  inbound: {
    vendor: {
      verify: Secret(config.webhookSecret),
    },
  },
});

Routing facade

HonoWebAdapter(postel, app) binds to your app and registers gated routes by source key. Raw bytes are verified before your handler runs; the verified result is on c.var.postel, typed to the source's schema output.

app.ts
import { Hono } from "hono";
import { HonoWebAdapter } from "@postel/hono";
import { postel } from "./lib/postel";

const app = new Hono();
const hwa = HonoWebAdapter(postel, app);

hwa.inbound.vendor.post("/webhooks/vendor", (c) => {
  const { event } = c.var.postel; // verified · typed (schema output) · raw bytes intact
  return c.json({ ok: true, type: event.type });
});

The source key (vendor) is type-checked against the sources you configured. On failure the gate short-circuits with the mapped status and your handler never runs; a non-PostelError bubbles as 5xx.

Other methods

Webhooks are POST by default, and .post is sugar for .on("POST", …). For a provider that delivers over PUT/PATCH, bind the method explicitly — only body-bearing verbs (POST | PUT | PATCH) are accepted, since the gate verifies a signature over the request body:

hwa.inbound.vendor.on("PUT", "/webhooks/vendor", (c) => c.json({ ok: true }));

JWKS

When you configure an outbound slot, hwa.outbound.bindJwks() publishes your current public keys at /.well-known/webhooks-keys, defaulting the provider to postel.outbound.keys.publicJwks() so rotations appear without a redeploy. Pass a route and/or provider to override.

hwa.outbound.bindJwks();                         // GET /.well-known/webhooks-keys
hwa.outbound.bindJwks("/keys", customProvider);  // custom route + provider

Admin

hwa.admin.bindAdminRoutes(prefix, opts) mounts the @postel/admin control plane (Hono speaks Fetch natively). authorize is required.

hwa.admin.bindAdminRoutes("/admin", { authorize: (req) => checkAdminToken(req) });

Low-level primitives

The facade is sugar over withWebhook / verifyWebhook. Reach for them to attach the gate to your own routing (extra middleware, custom paths). On this primitive path the context isn't statically typed, so read the verified result with getVerified(c) (it throws if the gate didn't run):

import { getVerified, verifyWebhook, withWebhook } from "@postel/hono";

app.post("/webhooks/vendor", verifyWebhook(postel.inbound.vendor), (c) => {
  const { event } = getVerified(c); // typed read on the primitive path
  return c.json({ ok: true, type: event.type });
});
app.post("/webhooks/github", withWebhook(postel.inbound.github, (c) => c.text("ok")));

On this page