Hono
Verify webhooks in Hono with HonoWebAdapter — a routing facade that gates each source, publishes JWKS, and mounts admin.
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.
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 + providerAdmin
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")));