SenebiclabsAPI referenceWebsiteGet an API key →
Delivery

Webhooks optional, signed

If you registered a webhook_url, we POST it once when the batch is delivered, so you do not have to poll. The body is the same shape as the delivered GET /results response:

POST https://your-app.com/hooks/senebiclabs
Content-Type: application/json
X-Senebiclabs-Signature: sha256=<hex>

{
  "event": "results.delivered",
  "project_id": "...",
  "company": "Your Company",
  "report": { ... },
  "items": [ ... ]
}

Verify the signature

Every webhook carries an X-Senebiclabs-Signature header. It is an HMAC-SHA256 of the exact request body, keyed with your webhook_secret. Recompute it and compare in constant time before you trust the payload. This proves the request came from us and was not altered in transit.

Compute over the raw request bytes, before any JSON parsing. Parsing and re-serialising can change the bytes and break the check.

import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

# FastAPI example
@app.post("/hooks/senebiclabs")
async def hook(request: Request):
    raw = await request.body()
    sig = request.headers.get("X-Senebiclabs-Signature", "")
    if not verify(raw, sig, WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    payload = json.loads(raw)   # trusted from here
    ...

Return 2xx to acknowledge. A 5xx or a refused connection is retried up to three times (immediately, then after 2s and 6s) — that pattern means your endpoint blipped. A 4xx is never retried: your service rejected the request itself, and repeating it would just deliver the same rejection.

The outcome is recorded and returned by GET /results once delivered, so a lost webhook is distinguishable from one that was never due:

"webhook": { "delivered": false, "status": 502, "attempts": 3, "at": "..." }

For an outage longer than the retries, or a changed URL, re-send it with POST /webhook/redeliver. GET /results remains the source of truth if delivery is critical.

Questions? senebiclabs@gmail.com