# Webhook Setup
Receive signed Klarefi events and verify webhook signatures
Source: https://www.klarefi.com/docs/guides/webhooks

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

> **Deployment gate**
>
> Self-serve webhook registration and delivery require
> `KLAREFI_ENABLE_SELF_SERVE_WEBHOOKS` to be enabled for the deployment. When
> it is disabled, self-serve intake creation returns
> `428 self_serve_webhooks_disabled`.

> **Use your workspace API base URL**
>
> Copy it from **Settings → Developer** before using the API example below.
> Current workspaces may use a `https://<deployment>.convex.site` URL.

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:

```bash
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.

```json
{
  "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.handoff_ready` - emitted when the customer marks hosted intake handoff complete
- `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.fact.evidence_updated` - emitted when an intake-review command updates a fact's evidence
- `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

```bash
npm install @klarefi/node
```

```typescript
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

```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:

```bash
npx klarefi webhooks test \
  --endpoint-url https://your-app.com/webhooks/klarefi \
  --signing-secret whsec_your_signing_secret
```

To verify a captured payload locally:

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

The same test delivery over HTTP:

```bash
curl -X POST "$KLAREFI_API_BASE_URL/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.
