Skip to content

Verifying webhooks

A callback_url receives an unauthenticated POST from the internet. Anything that learns the URL can post to it, so a handler that trusts the body will happily record an approval nobody gave.

Set a secret on the webhook in KirokuForms, pass the same string as webhook_secret, and check every delivery.

from kirokuforms import KirokuFormsHITL, WebhookVerificationError

client = KirokuFormsHITL(api_key="…", webhook_secret="whsec_…")

@app.post("/kiroku-webhook")
def kiroku_webhook(request):
    try:
        payload = client.verify_webhook(
            request.body,                                    # raw bytes or str
            request.headers.get("X-KirokuForms-Signature-256"),
        )
    except WebhookVerificationError as exc:
        return Response(status=400, body=str(exc))

    if payload["eventType"] == "hitl.task.completed":
        answers = payload["data"]["submission"]["data"]
        ...
    return Response(status=200)

How the signature works

KirokuForms computes:

HMAC-SHA256(secret, JSON.stringify(payload without its "signature" key))

hex encoded, and sends it two ways: in the X-KirokuForms-Signature-256 header, and as a signature field inside the JSON body.

That means the signed bytes are not the bytes on the wire: the delivered body has the signature added to it. Verifying requires parsing the JSON, dropping signature, and re-serialising exactly the way JSON.stringify would. verify_webhook does that, and its test suite checks it against signatures generated by the actual server-side signer rather than by a Python reimplementation agreeing with itself.

Pass the header when you have it. Omit it and the field inside the body is used, which is what you want in a framework that hands you parsed JSON and no headers.

Comparison is constant time. A wrong secret, a missing signature, a body that is not JSON, or a body edited after signing all raise WebhookVerificationError.

Before 0.3.0

webhook_secret was accepted, documented as being for verification, and read by nothing at all. If you built a callback endpoint from an earlier README, it is not verifying anything. Add the call above.