Overview
Verify Standard Webhooks signed requests sent to your application. What ships, what to read in which order.
The inbound half of Postel verifies signed webhook requests that other services send to your application. This is the half that's available today in @postel/core.
If you're sending webhooks to your customers — that's the outbound half, also available.
Start here
First time? Start with Verify a signed request. Before you ship to production, read the middle four — raw bytes, signing, key rotation, deduplication. They are the things that bite every webhook integration eventually.
Verify a signed request
The basic recipe — start here.
Raw bytes
Why JSON.stringify(JSON.parse(body)) silently breaks verification. The single most common failure.
Signing schemes
What verify() actually checks; v1 (HMAC) vs v1a (Ed25519).
Key rotation
The multi-secret window; JWKS for asymmetric verification.
Deduplication
Webhooks are at-least-once; the atomic dedup helper.
Web adapters
Wire verification into Hono, Express, Fastify, NestJS, or any runtime.
What you get
| Feature | Status | Where |
|---|---|---|
Postel({ inbound }) factory with typed per-source configuration | Ships | @postel/core |
HMAC-SHA256 (v1) signature verification (Standard Webhooks default) | Ships | Secret(...) strategy |
Ed25519 (v1a) signature verification (Postel extension) | Ships | PublicKey(...) strategy |
| JWKS consumer with caching and auto-refresh | Ships | Keyset({ jwksUri }) strategy |
| Multi-secret rotation windows and cross-scheme migration | Ships | Pass [Verifier, Verifier, …] |
| Custom verification schemes — plug your own | Ships | Implement the Verifier contract |
| Skip verification at a trusted boundary | Ships | Noop() strategy |
| Constant-time signature comparison | Ships | (always on) |
Structured PostelError subclasses for every failure mode | Ships | SignatureInvalid, TimestampTooOld, MalformedHeader, UnknownKeyId, RawBytesMismatchDetected |
| Idempotency dedup with Postgres / SQLite / MySQL / in-memory adapters | Ships | @postel/pg, @postel/sqlite, @postel/mysql, InMemoryDedup |
| Hono, Express, Fastify, NestJS framework adapters | Ships | @postel/hono, @postel/express, @postel/fastify, @postel/nestjs — a verification gate over @postel/http |
| Next.js, Bun adapters | Stub packages today (__postelPackage only); use the factory directly meanwhile | @postel/nextjs, @postel/bun |
If a row above isn't on this list, it's outbound — see Outbound.
The mental model
A verified webhook is just an HTTP POST with three extra headers:
webhook-id: msg_2k4n8...
webhook-timestamp: 1716553200
webhook-signature: v1,h7P3wKjQz...The receiver's job is to reject any request that doesn't match a small, well-defined set of conditions — and to do so in a way that doesn't leak timing information. Postel's verify() does this in five steps; failures throw subclasses of PostelError that name which step failed. See Signing schemes for the full pipeline.
You wire this once per source with the Postel({ inbound }) factory and call postel.inbound.<source>.verify(rawBytes, headers) from your route handler. That's the whole receiver surface.