Standard Webhooks
Compliant by default
Headers, signature schemes (HMAC v1 + Ed25519 v1a), payload envelope, and prefixes follow the Standard Webhooks spec. JWKS publication is a one-liner.
Be conservative in what you send · liberal in what you accept
Postel is a polyglot webhooks library backed by executable specs. Standard Webhooks-compliant. Sender plus receiver. Runs inside your application against your Postgres or SQLite database.
Verify a webhook
verify returns the parsed event on success, or throws a structured error naming the failing step: SignatureInvalid, TimestampTooOld, MalformedHeader, and friends.
import { verify, SignatureInvalid } from "@postel/edge";
export async function POST(req: Request) {
const body = new Uint8Array(await req.arrayBuffer());
const headers = Object.fromEntries(req.headers);
try {
const { event } = await verify(body, headers, process.env.WEBHOOK_SECRET!);
console.log(event.type, event.data);
return new Response("ok");
} catch (err) {
if (err instanceof SignatureInvalid) return new Response("bad signature", { status: 401 });
throw err;
}
}Planned — this API reflects the target design. The package is not published yet.
import postel "github.com/postel-sh/postel-go"
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
event, err := postel.Verify(body, r.Header, os.Getenv("WEBHOOK_SECRET"))
if err != nil {
var sigErr *postel.SignatureInvalid
if errors.As(err, &sigErr) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("received: %s", event.Type)
w.WriteHeader(http.StatusOK)
}Planned — this API reflects the target design. The package is not published yet.
from postel import verify, SignatureInvalid
@app.post("/webhooks")
def handle_webhook():
body = request.get_data()
headers = dict(request.headers)
try:
event = verify(body, headers, os.environ["WEBHOOK_SECRET"])
print(f"received: {event.type}")
return "ok", 200
except SignatureInvalid:
abort(401, "bad signature")Planned — this API reflects the target design. The package is not published yet.
use axum::{body::Bytes, http::{HeaderMap, StatusCode}, response::IntoResponse};
use postel::{verify, PostelError};
async fn handle_webhook(headers: HeaderMap, body: Bytes) -> impl IntoResponse {
let secret = std::env::var("WEBHOOK_SECRET").unwrap();
match verify(&body, &headers, &secret).await {
Ok(event) => {
println!("received: {}", event.r#type);
(StatusCode::OK, "ok")
}
Err(PostelError::SignatureInvalid(_)) => {
(StatusCode::UNAUTHORIZED, "bad signature")
}
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "error"),
}
}Standard Webhooks
Headers, signature schemes (HMAC v1 + Ed25519 v1a), payload envelope, and prefixes follow the Standard Webhooks spec. JWKS publication is a one-liner.
Edge-first
@postel/edge ships unmodified on Cloudflare Workers, Vercel Edge, Deno Deploy, and Bun. Web Crypto only — no node:* imports, no polyfills.
Library, not service
Outbox inserts join your existing transaction. No separate dispatcher, no Redis, no broker. The library you embed; not the service you stand up.
Status
@postel/edge ships today: verify, JWKS consumer, dedup helper, multi-secret rotation, raw-bytes preservation. Sender (postel.send, outbox, retries, fanout, replay) lands in v0.2.0. Go, Python, and Rust ports follow.