Webhook Setup
Receive signed Klarefi events and verify webhook signatures
Overview
Klarefi sends signed, versioned domain events for intake, case, review, document, and webhook delivery activity. Use webhooks to learn when a Hosted Intake case changes, then read the current case from the API.
All webhook deliveries are sent to registered endpoints and signed with HMAC-SHA256. Per-request webhook URLs are rejected; configure endpoint URLs in the dashboard so delivery policy, retries, and signing secrets stay consistent.
Setting Up Webhooks
1. Register an Endpoint
Register a webhook endpoint in the Klarefi dashboard under Webhooks. You'll
receive an endpoint signing secret (whsec_...) to verify incoming events.
Secrets are scoped to a registered endpoint and are shown only once.
2. Implement Your Receiver
Your endpoint must:
- Accept POST requests with JSON body
- Return a 2xx status code within 30 seconds
- Verify the signature before processing
- Process duplicate deliveries idempotently using the stable
delivery_id
For a Next.js starter route:
npx klarefi webhooks init --framework nextSignature Verification
Every webhook includes an X-Klarefi-Signature header:
X-Klarefi-Signature: t=1704067200,v1=abc123def456...The signature is computed as: HMAC-SHA256(signing_secret, timestamp + "." + raw_body)
Event Payloads
Events use a versioned name and carry schema metadata. The event name is stable within a version; additive fields may appear over time, so ignore unknown keys.
{
"event_id": "evt_01J7W9S4TSQ0T4SH43JYB9W5R8",
"delivery_id": "whd_01J7W9S52C2ZK1XFY2GM7Z7SAK",
"event_type": "v1.case.completed",
"schema_version": "v1",
"trace_id": "trc_01J7W9S4V8ZKH4W4B8EG9J7TKC",
"created_at": "2026-05-01T12:34:56.000Z",
"data": {
"case_status": "completed"
}
}delivery_id is stable for a delivery attempt series and should be used for
idempotency. event_id identifies the domain event. trace_id can be used when
correlating dashboard delivery logs with receiver logs.
Event Catalog
Webhook endpoints can subscribe to individual event types or * for the
current catalog:
v1.intake.session_createdv1.intake.session_expiredv1.intake.submittedv1.case.processingv1.case.applicant_blockedv1.case.needs_reviewv1.case.completedv1.case.failedv1.review.completedv1.document.processedv1.document.failedv1.intake.gap_resolvedv1.webhook.delivery_failed*
The API test endpoint sends a signed v1.webhook.test payload. It is not a
subscription event; use it only to verify receiver parsing and signatures.
TypeScript
npm install @klarefi/nodeimport { constructEvent, KlarefiWebhookSignatureError } from "@klarefi/node";
import express from "express";
const app = express();
app.post(
"/webhooks/klarefi",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const event = constructEvent(
req.body.toString(),
req.headers["x-klarefi-signature"] as string,
"whsec_your_signing_secret",
);
console.log("Received event:", event.event_type, event.delivery_id);
res.sendStatus(200);
} catch (error) {
if (error instanceof KlarefiWebhookSignatureError) {
res.sendStatus(401);
return;
}
console.error("Webhook handling failed:", error);
res.sendStatus(500);
}
},
);Python
import hashlib
import hmac
import json
import time
from flask import Flask, request
app = Flask(__name__)
def verify_webhook_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> dict:
parts = dict(part.split("=", 1) for part in signature_header.split(",") if "=" in part)
timestamp = int(parts["t"])
if abs(int(time.time()) - timestamp) > 300:
raise ValueError("stale signature")
expected = hmac.new(
signing_secret.encode(),
str(timestamp).encode() + b"." + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, parts["v1"]):
raise ValueError("invalid signature")
return json.loads(raw_body)
@app.route("/webhooks/klarefi", methods=["POST"])
def handle_webhook():
try:
event = verify_webhook_signature(
raw_body=request.get_data(),
signature_header=request.headers.get("X-Klarefi-Signature"),
signing_secret="whsec_your_signing_secret",
)
if event["event_type"] == "v1.case.completed":
print("Case completed:", event["case_id"])
# Read the current case or package from the API.
return "", 200
except Exception as e:
print("Webhook verification failed:", e)
return "", 401Event Handling Guidance
Treat webhook events as integration signals, then read the current case or session state from the API when you need the latest result.
This avoids coupling your integration to a long list of event-specific assumptions.
Testing Webhooks
Send a test event to verify your receiver works before going live:
npx klarefi webhooks test \
--endpoint-url https://your-app.com/webhooks/klarefi \
--signing-secret whsec_your_signing_secretTo verify a captured payload locally:
npx klarefi webhooks verify \
--payload payload.json \
--signature "t=1704067200,v1=..." \
--secret whsec_your_signing_secretThe same test delivery over HTTP:
curl -X POST https://api.klarefi.com/api/v1/webhooks/test \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://your-app.com/webhooks/klarefi",
"signing_secret": "whsec_your_signing_secret"
}'Retry Policy
If delivery fails, Klarefi retries with backoff and marks the delivery as
pending, delivering, retrying, delivered, or failed in the dashboard.
Exhausted deliveries are moved to the dead-letter queue and can emit
v1.webhook.delivery_failed to subscribed endpoints. Use the dashboard delivery
logs, delivery_id, and trace_id to debug failures or replay failed
deliveries.