Web adapters

Express

Verify webhooks in Express with ExpressWebAdapter — gated routes that capture raw bytes and set req.postel.

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

ExpressWebAdapter(postel, app) registers gated routes by source key. Each route mounts express.raw({ type: () => true }) ahead of the gate for you, so the verifier sees the exact received bytes — don't put express.json() in front. The verified result is on req.postel.

app.ts
import express from "express";
import { ExpressWebAdapter } from "@postel/express";
import { postel } from "./lib/postel";

const app = express();
const ewa = ExpressWebAdapter(postel, app);

ewa.inbound.vendor.post("/webhooks/vendor", (req, res) => {
  res.json({ ok: true, type: req.postel.event.type });
});

Source keys are type-checked. On failure the gate writes the mapped 4xx and your handler never runs; a non-PostelError is forwarded to next(err) (your error middleware / 500).

Other methods

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

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

JWKS

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

bindJwks() defaults the provider to postel.outbound.keys.publicJwks(); available when an outbound slot is configured.

Admin

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

Low-level primitives

The facade wraps withWebhook / verifyWebhook (each returns [express.raw(...), gate]) and the fetchToExpress bridge:

import { verifyWebhook, fetchToExpress } from "@postel/express";
import { adminRouter } from "@postel/admin";

app.post("/webhooks/vendor", verifyWebhook(postel.inbound.vendor), (req, res) => res.send("ok"));
app.use("/admin", fetchToExpress(adminRouter(postel, { authorize: (req) => checkAdminToken(req) })));

On this page