GenniDocs

Guides

Webhooks

Instead of polling, register a URL and Genni will POST to it when one of your documents changes status. Add webhooks in the portal under Webhooks, or through the API:

bash
curl https://api.genni.scribblegreen.com/v1/webhooks -H "Authorization: Bearer $GENNI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example/genni", "events": ["document.completed", "document.in_review"]}'

The response includes a signing secret (whsec_…), shown once. Leave events empty to receive all of them. URLs must be public https addresses.

EventSent when
document.completedFigures are extracted and every check passed, or review finished.
document.in_reviewFigures are extracted but a check needs a person.
document.analyzedProcessing finished without extracting figures.
document.failedProcessing failed after retries.

What Genni sends

http
POST /genni HTTP/1.1
Content-Type: application/json
Genni-Event: document.completed
Genni-Delivery: 7d6c0a57-…
Genni-Signature: t=1790505600,v1=5f2b…

{"id": "7d6c0a57-…", "type": "document.completed", "created_at": "2026-09-27T16:05:00Z",
 "data": {"document": {"id": "…", "status": "completed", "company_name": "Zenith Bank Plc", "fiscal_year": 2024,
   "external_ref": "doc-8841", "open_review_items": 0, "figures_url": "/v1/documents/…/figures", …}}}

Verify the signature

Compute an HMAC-SHA256 of t + "." + raw body with your secret and compare it with v1. Use the raw request body, before any JSON parsing, and reject timestamps older than a few minutes.

python
import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts["v1"], expected)
javascript
import crypto from "node:crypto";

export function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

Retries

Reply with any 2xx status within 10 seconds. Otherwise Genni retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then marks the delivery failed. The Genni-Delivery ID stays the same across retries, so you can ignore repeats. Every attempt appears in the delivery log.

Next: Errors and limits →