Oligon Receipts is in private beta — request access.
Guides
Webhook handlers (per language)

Webhook handler reference implementations

Drop-in handlers for the frameworks we see most often. Each one:

  1. Reads the raw body (no JSON middleware before this step).
  2. Verifies the HMAC-SHA256 signature.
  3. Parses JSON.
  4. Dispatches on event.type.
  5. 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.