Webhooks
Webhooks deliver asynchronous events to your server: a long-running extraction completes, a member is invited, a billing event fires. Each delivery is signed with HMAC-SHA256 so you can verify authenticity.
Setup
- Portal → Webhooks → Add endpoint.
- Enter a public HTTPS URL.
- Select the event types to subscribe to (or "All").
- Copy the signing secret (
whsec_…). Store it like an API key.
Event types
| Event | Fires when |
|---|---|
receipt.completed | Extraction finished successfully. |
receipt.failed | Extraction errored out (low confidence, unsupported document, etc.). |
apikey.created | An admin minted a new API key. |
apikey.revoked | A key was revoked or expired. |
member.invited | A new teammate was invited. |
billing.usage_threshold | Org crossed 80% / 100% of its monthly quota. |
Payload
{
"id": "evt_01HQX...",
"type": "receipt.completed",
"org_id": "org_01HQX...",
"created_at": "2026-06-08T14:32:11Z",
"data": {
"receipt": { "id": "rcp_01HQX...", "status": "completed", "total": "47.50" }
}
}Verifying the signature
We send a header X-Oligon-Signature: sha256=<hex> over the raw,
unaltered request body.
from fastapi import FastAPI, Request, HTTPException
from oligon_receipts import OligonReceipts
app = FastAPI()
SECRET = "whsec_..."
@app.post("/webhooks/oligon")
async def handler(request: Request):
body = await request.body()
sig = request.headers.get("x-oligon-signature", "")
if not OligonReceipts.verify_webhook(body, sig, SECRET):
raise HTTPException(400, "invalid signature")
event = await request.json()
# ... dispatch on event["type"]
return {"ok": True}Read the body before any framework deserialises it. Frameworks like Express, Koa, and FastAPI may re-encode JSON (adding whitespace) which invalidates the signature.
Retries
Failed deliveries are retried with exponential backoff over 72 hours:
T+0s T+10s T+1m T+5m T+30m T+2h T+6h T+12h T+24h T+48h T+72hWe consider any 2xx within 10 s as a success. After 72 h the event lands
in Webhook Events → Failed and stays queryable via /v1/webhooks/events
for 30 days.
Idempotency
Always design your handler to be idempotent: dedupe on event.id, not on
receipt.id. The same event can be delivered more than once during a
network partition.