Guides

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 next

Signature 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_created
  • v1.intake.session_expired
  • v1.intake.submitted
  • v1.case.processing
  • v1.case.applicant_blocked
  • v1.case.needs_review
  • v1.case.completed
  • v1.case.failed
  • v1.review.completed
  • v1.document.processed
  • v1.document.failed
  • v1.intake.gap_resolved
  • v1.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/node
import { 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 "", 401

Event 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_secret

To verify a captured payload locally:

npx klarefi webhooks verify \
  --payload payload.json \
  --signature "t=1704067200,v1=..." \
  --secret whsec_your_signing_secret

The 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.

On this page