Webhook handler reference implementations
Drop-in handlers for the frameworks we see most often. Each one:
- Reads the raw body (no JSON middleware before this step).
- Verifies the HMAC-SHA256 signature.
- Parses JSON.
- Dispatches on
event.type. - Responds 200 within 10 seconds (heavy work goes on a queue).
FastAPI
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from oligon_receipts import OligonReceipts
app = FastAPI()
SECRET = "whsec_..."
@app.post("/webhooks/oligon")
async def handle(request: Request, bg: BackgroundTasks):
body = await request.body()
if not OligonReceipts.verify_webhook(body, request.headers.get("x-oligon-signature", ""), SECRET):
raise HTTPException(400, "invalid signature")
event = await request.json()
if event["type"] == "receipt.completed":
bg.add_task(process_receipt, event["data"]["receipt"]["id"])
return {"ok": True}
async def process_receipt(receipt_id: str):
# heavy work — DB write, push notification, etc.
...Flask
from flask import Flask, request, abort, jsonify
from oligon_receipts import OligonReceipts
app = Flask(__name__)
SECRET = "whsec_..."
@app.post("/webhooks/oligon")
def handle():
body = request.get_data()
if not OligonReceipts.verify_webhook(body, request.headers.get("X-Oligon-Signature", ""), SECRET):
abort(400, "invalid signature")
event = request.get_json()
# enqueue work
return jsonify(ok=True)Express
import express from "express";
import { verifyWebhook } from "@oligon/receipts";
const app = express();
const SECRET = process.env.WEBHOOK_SECRET!;
app.post(
"/webhooks/oligon",
// CRITICAL: raw body, not JSON middleware
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["x-oligon-signature"] as string;
if (!(await verifyWebhook(req.body, sig, SECRET))) {
return res.status(400).send("invalid signature");
}
const event = JSON.parse(req.body.toString());
queue.add("oligon-event", event);
res.status(200).send("ok");
},
);Next.js (App Router)
// app/api/webhooks/oligon/route.ts
import { NextResponse } from "next/server";
import { verifyWebhook } from "@oligon/receipts";
export async function POST(req: Request) {
const body = await req.text(); // raw
const sig = req.headers.get("x-oligon-signature") ?? "";
if (!(await verifyWebhook(body, sig, process.env.WEBHOOK_SECRET!))) {
return new NextResponse("invalid signature", { status: 400 });
}
const event = JSON.parse(body);
await queue.add("oligon-event", event);
return NextResponse.json({ ok: true });
}Cloudflare Workers
import { verifyWebhook } from "@oligon/receipts";
export default {
async fetch(req: Request, env: Env) {
if (req.method !== "POST") return new Response("method", { status: 405 });
const body = await req.text();
const sig = req.headers.get("x-oligon-signature") ?? "";
if (!(await verifyWebhook(body, sig, env.WEBHOOK_SECRET))) {
return new Response("invalid signature", { status: 400 });
}
const event = JSON.parse(body);
await env.QUEUE.send(event);
return new Response("ok");
},
};Go (net/http)
package main
import (
"encoding/json"
"io"
"log"
"net/http"
"os"
oligon "github.com/oligon/receipts-go"
)
func handle(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if !oligon.VerifyWebhook(body, r.Header.Get("X-Oligon-Signature"), os.Getenv("WEBHOOK_SECRET")) {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
var evt oligon.WebhookEvent
_ = json.Unmarshal(body, &evt)
log.Println("got", evt.Type)
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhooks/oligon", handle)
log.Fatal(http.ListenAndServe(":8080", nil))
}Go (gin)
import (
"github.com/gin-gonic/gin"
oligon "github.com/oligon/receipts-go"
)
r.POST("/webhooks/oligon", func(c *gin.Context) {
body, _ := io.ReadAll(c.Request.Body)
if !oligon.VerifyWebhook(body, c.GetHeader("X-Oligon-Signature"), os.Getenv("WEBHOOK_SECRET")) {
c.AbortWithStatus(400)
return
}
var evt oligon.WebhookEvent
_ = json.Unmarshal(body, &evt)
enqueue(evt)
c.JSON(200, gin.H{"ok": true})
})Local testing
Use ngrok (opens in a new tab) or cloudflared tunnel to forward
events to localhost. Replay any historical event from the Webhooks
Events tab — useful for stepping through the handler in a debugger.