{
  "markdown": "# Kaval clients\n\nOpen-source client libraries for [Kaval](https://usekaval.com). **Register the payers and pages you\ncare about once. Kaval watches them, extracts structured records against a schema you define, and\ndelivers each extraction — plus a monthly PDF + manifest rollup — as a webhook the moment it\nlands, instead of you polling or re-researching it.** `check()` is the second half: before an agent\nacts on one of those facts, send Kaval the action and it answers `ALLOW`, `REVIEW`, or `BLOCK` with a\nsigned receipt.\n\n**Policy engines decide whether an action is permitted under the rules; Kaval verifies whether the\nfacts those rules depend on are still true.**\n\nThese are **thin HTTP clients** for the hosted Kaval API (`https://api.usekaval.com`). Create an API\nkey at [usekaval.com](https://usekaval.com).\n\n| Package                         | Language          | Install                 | Source                       |\n| ------------------------------- | ----------------- | ----------------------- | ---------------------------- |\n| [`@usekaval/kaval`](sdks/node)  | Node / TypeScript | `npm i @usekaval/kaval` | [sdks/node](sdks/node)       |\n| [`kaval`](sdks/python)          | Python            | `pip install kaval`     | [sdks/python](sdks/python)   |\n| [`@usekaval/mcp`](packages/mcp) | MCP server        | `npx -y @usekaval/mcp`  | [packages/mcp](packages/mcp) |\n\nThe 0.7.3 portfolio methods are available in the Node SDK and MCP server.\n\nThe Python SDK does not yet expose contracts, fact imports, bulletins, or training review.\n\n## Sources → Extractions → Webhooks\n\nThe primary loop needs no LLM call and no polling loop of your own:\n\n```ts\nimport { Kaval } from \"@usekaval/kaval\";\n\nconst kaval = new Kaval({ apiKey: process.env.KAVAL_API_KEY });\n\n// 1. Watch a payer.\nconst { source } = await kaval.addSource({\n  kind: \"entity\",\n  name: \"Aetna\",\n  intent: \"payer policy bulletins\",\n});\n\n// 2. Register the shape you want extracted, and bind it to the source.\nconst schema = await kaval.createExtractionSchema({\n  name: \"prior-auth-bulletin\",\n  json_schema: {\n    type: \"object\",\n    properties: { cpt_code: { type: \"string\" }, requires_prior_auth: { type: \"boolean\" } },\n    required: [\"cpt_code\", \"requires_prior_auth\"],\n  },\n});\nawait kaval.updateSource({ id: source.id, extraction_schema_id: schema.id });\n// reprocess: true also re-extracts versions that already ran under another schema\n// (webhook source_change: \"schema_changed\"; join on source_version_id).\n\n// 3. Get pushed an extraction.document webhook every time a new bulletin lands, already\n//    extracted against the schema — or poll listExtractionRuns() for the same records.\nconst { webhook_verification } = await kaval.subscribeExtractions({\n  callback_url: \"https://your-app.example.com/hooks/kaval\",\n});\n```\n\n```py\nimport os\n\nfrom kaval import KavalClient\n\nkaval = KavalClient(api_key=os.environ[\"KAVAL_API_KEY\"])\n\nsource = kaval.add_source(kind=\"entity\", name=\"Aetna\", intent=\"payer policy bulletins\")\nschema = kaval.create_extraction_schema(\n    name=\"prior-auth-bulletin\",\n    json_schema={\n        \"type\": \"object\",\n        \"properties\": {\"cpt_code\": {\"type\": \"string\"}, \"requires_prior_auth\": {\"type\": \"boolean\"}},\n        \"required\": [\"cpt_code\", \"requires_prior_auth\"],\n    },\n)\nkaval.update_source(source[\"id\"], extraction_schema_id=schema[\"id\"])\n# reprocess=True also re-extracts versions that already ran under another schema\n# (webhook source_change=\"schema_changed\"; join on source_version_id).\nkaval.subscribe_extractions(callback_url=\"https://your-app.example.com/hooks/kaval\")\n```\n\nNo schema, or want a one-off pull instead of waiting for the next document? `createExtractionRun({\npublisher_id, period, extraction_schema_id })` requests a single publisher + period run on demand;\n`getExtractionRun()` / `listExtractionRuns()` report its lifecycle\n(`processing` → `retry` → `succeeded` / `review_required` / `failed`), and\n`listExtractionPackages()` lists the monthly PDF + manifest rollup each publisher/period is packaged\ninto. This is the schema-bound successor to the free-text bulletin methods (`listBulletins()`,\n`getBulletin()`), which are soft-deprecated but keep working.\n\n`check()` is what you call next, right before an agent acts on a fact this loop delivered — it is\ncovered in the next section.\n\n> **0.6 was a breaking release.** Nine MCP tools collapsed to seven (before later portfolio/extraction tools landed). The whole verification\n> surface collapsed to one call. Every removed endpoint now answers a structured\n> `410 {\"error\":\"tool_retired\",\"replacement\":\"/v1/check\"}`, and the clients translate that into an\n> error that names `check` by name. See [Migrating from 0.5](#migrating-from-05).\n\n## Optional: verify before an agent acts\n\n`check()` is not required to keep facts current — the webhook loop above does that — but it is the\ncall to make right before an agent relies on one, because it re-derives the verdict from current\nstate and hands back a signed receipt:\n\n```ts\nimport { Kaval } from \"@usekaval/kaval\";\n\nconst kaval = new Kaval({ apiKey: process.env.KAVAL_API_KEY });\n\nconst result = await kaval.check({\n  action: \"Approve this prior-authorization request at the in-network rate\",\n  context: \"payer: Aetna; CPT 12345; plan HMO\",\n  materiality: \"critical\",\n});\n\nif (result.decision !== \"ALLOW\") {\n  // REVIEW is never permission to act.\n  holdForHuman(result.facts.filter((fact) => fact.status !== \"holds\"));\n}\n```\n\n```py\nimport os\n\nfrom kaval import KavalClient\n\nkaval = KavalClient(api_key=os.environ[\"KAVAL_API_KEY\"])\nresult = kaval.check(action=\"Approve this prior-authorization request at the in-network rate\")\nif result[\"decision\"] != \"ALLOW\":\n    hold_for_human(result[\"facts\"])\n```\n\nWhat comes back:\n\n| field          | meaning                                                                                              |\n| -------------- | ---------------------------------------------------------------------------------------------------- |\n| `decision`     | `ALLOW` (every material fact holds on fresh evidence) · `REVIEW` · `BLOCK`                           |\n| `reason_codes` | why, from a closed eight-code taxonomy                                                               |\n| `facts[]`      | one row per fact: `status` (`holds`/`changed`/`unknown`), `materiality`, and the sources it rests on |\n| `receipt`      | `{ id, signature, signed_at }` — fetch the full signed document with `getReceipt(id)`                |\n| `latency_ms`   | `{ compile, lookup, live, total }`                                                                   |\n\nA check on facts a watched source already covers is a database read: no model call, no fetch,\nnothing on the wire. A **cold** check does live research before it answers — search, fetch, adjudicate —\nand the server lets that run for up to 100s by default, so give the call room. `mode: \"fast\"`\n(equivalently `max_wait_ms: 0`) skips research entirely and reports anything it could not settle as\n`unknown`, which is `REVIEW`.\n\nThe decision table is published, so the receipt's fact list re-derives the verdict offline, and the\nEd25519 public keys are served unauthenticated at `GET /v1/proof-verification-keys/:kid` — checking a\nreceipt needs no Kaval account and no API key.\n\nThe verifier that does it for you ships **inside the SDK**: `@usekaval/kaval/verify` is a\ndependency-free subpath export of `@usekaval/kaval`, and the same package ships a\n`kaval-receipt-verify` CLI. Neither needs a Kaval account, an API key, or Kaval's database; the only\nrequest either can make is for the public keyset, and that request is optional. Hand it a receipt and\na keyset. It answers three questions by default and one optional verdict question:\n\n1. **Cryptographic validity** — does the Ed25519 signature cover the exact canonical unsigned bytes?\n2. **Key trust** — is that `key_id` active or benignly retired, rather than revoked or compromised?\n3. **Freshness** — `fresh`, `recheck_due`, `expired`, `not_yet_issued`, or `unknown`.\n4. **Verdict derivation** — does the receipt's fact list produce its stated verdict and reason codes?\n\nA valid signature proves who sealed those exact bytes. It does not prove the claim is still true, or\nthat the key is still trusted, which is why the three answers never collapse into one boolean.\n\n```ts\nimport {\n  extractReceipt,\n  parseJsonStrict,\n  verifyReceipt,\n} from \"@usekaval/kaval/verify\";\n\nconst receipt = extractReceipt(parseJsonStrict(receiptText));\nconst result = verifyReceipt(receipt, parseJsonStrict(keysetText), {\n  derive_verdict: true,\n});\n\nresult.cryptographic.valid; // the signature covers these exact canonical bytes\nresult.key.trusted; // the signing key is not revoked or compromised\nresult.freshness.status; // separate fact — a check receipt carries no expiry, so `unknown`\nresult.decision?.matches; // the published table reproduced the verdict and reason codes\n```\n\n```bash\n# Reproducible audit: archive the keyset beside the receipt and stay entirely offline.\nnpx -p @usekaval/kaval kaval-receipt-verify verify receipt.json --keyset keys.json\nnpx -p @usekaval/kaval kaval-receipt-verify verify receipt.json --keyset keys.json --derive-verdict\n\n# Or resolve the key over HTTPS from the unauthenticated endpoint.\nnpx -p @usekaval/kaval kaval-receipt-verify verify receipt.json \\\n  --key-url https://api.usekaval.com/v1/proof-verification-keys\n```\n\nExit `0` means the signature is valid and the key is trusted; a stale receipt still exits `0`,\nbecause freshness is a separate fact — pass `--require-fresh` to make anything but `fresh` non-zero.\nExit `1` is a completed but unaccepted verification, `2` an input, I/O, or discovery failure. Parse\nuntrusted receipt text with `parseJsonStrict`, not `JSON.parse`: duplicate members and lossy numbers\nare evidence, and `JSON.parse` throws that evidence away before any verifier can see it.\n\n## Keep it warm: watch the sources\n\nA check is a database read when the facts it needs are already backed by a watched source, and a\nbounded research run when they are not. Registering the _name_ of an authority is usually enough:\n\n```ts\nawait kaval.addSource({\n  kind: \"entity\",\n  name: \"Aetna\",\n  intent: \"payer policy bulletins\",\n});\n```\n\nKaval resolves that to the pages that publish it, polls them adaptively (slower when nothing\nchanges, faster when it does), and re-evaluates the dependent facts when they move. `kind: \"url\"`\nwatches one page; `kind: \"push\"` is a document your own system sends in with `sendEvent()`. You do\nnot have to register first — a source a check cites is auto-watched — but registering ahead of time\nis what makes the _first_ check on a fact fast.\n\nNaming an entity is what enqueues the discovery that works out _how_ to acquire the pages behind it.\nA `kind: \"url\"` source registered directly does not get one, so `recompileSource(id)` is how you ask\nfor one — and it is also the only way back once a source's acquisition plan breaks. It answers `202\n{ source_id, job_id, created }`; `created: false` means an open job already covered it.\n\n## Close the loop: subscribe to deltas\n\nWatching is only half of it. Subscribe to `fact_state.delta` and Kaval pushes you what changed and\nwhat it flipped, instead of you discovering it on the next check:\n\n```ts\nconst { webhook_verification } = await kaval.subscribeFactStateDeltas({\n  callback_url: \"https://your-app.example.com/hooks/kaval\",\n  external_scope_ids: [\"plan:HMO\"], // optional filter\n});\n// Store webhook_verification.secret — it is shown exactly once, and it is how you\n// authenticate every inbound delivery.\n```\n\nEach delivery names the source, the old and new content hashes, a diff summary, and the facts whose\nstate changed — `{fingerprint, text, old_state → new_state, basis}` — plus a pointer to the receipt\ncovering the re-evaluation. Manage subscriptions with `listWebhooks()`, `setWebhookEnabled()`,\n`deleteWebhook()`, and re-drive a dead letter with `replayWebhookDelivery()`.\n\n## Push your own documents\n\nFor documents Kaval cannot fetch — a contract, an internal policy, a customer upload — push the new\nversion and let the background loop do the rest:\n\n```ts\nconst { changed, facts_pending_review } = await kaval.sendEvent({\n  namespace: \"contracts\",\n  document_id: \"msa-2026-07\",\n  content: extractedText,\n  scope_keys: [\"contract:msa-2026-07\"],\n});\n```\n\nKaval diffs it against the previous version, marks the dependent facts stale, re-evaluates them, and\nemits the delta webhook. Checks that land mid-re-evaluation honestly return `REVIEW`.\n\n## MCP\n\n```bash\nKAVAL_API_KEY=kv_live_… npx -y @usekaval/mcp\n```\n\nThirty-one tools run over stdio. They cover checks, receipts, contracts, bulk imports,\nextractions (extraction schemas, runs, monthly packages), the soft-deprecated bulletin tools, training\nreview, watched sources, outcomes, and the deprecated `verify` alias.\n\nEleven JSON resources expose contract issues, bulletin extraction status, and the prior read models.\nExplicit consent is available, but model promotion and bulletin requeue remain internal. See [packages/mcp](packages/mcp).\n\nMCP clients cancel a tool call well before 100s, so the server narrows `check`'s research budget to\nfit inside that envelope instead of inheriting the full default. A cold check over MCP therefore\ncomes back `REVIEW` more often than the same call through an SDK — register the sources it depends\non and the warm path removes the difference.\n\n## Migrating from 0.5\n\nEverything below folded into `check`. The old routes answer `410 tool_retired`; the clients raise\n`KavalRetiredError` (Node) / `KavalRetiredError` (Python) naming the replacement, and the MCP server\nreturns `{\"error\":\"tool_retired\"}` with a message telling the agent to call `check`.\n\n### MCP tools\n\n| 0.5 tool                        | 0.6                                                                                 |\n| ------------------------------- | ----------------------------------------------------------------------------------- |\n| `currentness_check`             | `check` — `{ action }` or `{ claims: [\"…\"] }`                                       |\n| `currentness_verify`            | `check` — branch on `decision === \"ALLOW\"` instead of `act`                         |\n| `currentness_extract_and_check` | `check` — pass the paragraph as `action`/`context`; Kaval compiles the facts itself |\n| `currentness_scan_store`        | `check` — `{ claims: [...] }`, up to 20 per call                                    |\n| `currentness_monitor`           | `add_source` + a `fact_state` webhook subscription — deltas are pushed, not swept   |\n| `proof_audit`                   | `check` — the receipt **is** the proof; `get_receipt` fetches it in full            |\n| `proof_gate`                    | `check` — the warm path re-checks from stored state; nothing to re-apply separately |\n| `report_outcome`                | `report_outcome` (unchanged; pass `receipt.id`)                                     |\n| `verify`                        | `verify`, now deprecated → move to `check`                                          |\n\n### Node / Python methods\n\n| 0.5                                                 | 0.6                                                                        |\n| --------------------------------------------------- | -------------------------------------------------------------------------- |\n| `audit()`, `gate()`, `gateAction()`/`gate_action()` | `check()`                                                                  |\n| `check(belief)` / `check(belief=…)`                 | `check({ action })` / `check(action=…)`                                    |\n| `verifyBelief()` / `legacy_verify_belief()`         | `check({ action, context })`                                               |\n| `extractAndCheck()` / `extract_and_check()`         | `check({ action: theText })`                                               |\n| `scanStore()` / `scan_store()`                      | `check({ claims: [...] })`                                                 |\n| `monitor()`                                         | `addSource()` + `subscribeFactStateDeltas()`                               |\n| `kaval()` / `kavalBatch()` / `kaval_batch()`        | `check({ claims: [{subject, predicate, object, scope}] })`                 |\n| `verify()`                                          | `verify()`, deprecated → `check()`                                         |\n| —                                                   | new: `getReceipt`, `listSources`, `recompileSource`, `sendEvent`, webhooks |\n| `ProofNotFoundError` / `KavalProofNotFoundError`    | removed with `/v1/gate`                                                    |\n\n### Verdict mapping\n\n| 0.5 belief status                              | 0.6                                                 |\n| ---------------------------------------------- | --------------------------------------------------- |\n| `current` + `act: true`                        | `decision: \"ALLOW\"`, every fact `holds`             |\n| `stale` / `contradicted`                       | fact `changed` → `REVIEW` or `BLOCK` by materiality |\n| `unsupported` / `insufficient` / `conflicting` | fact `unknown` → `REVIEW` (`BLOCK` if critical)     |\n\n## Idempotency\n\nContract mutations, fact imports, extraction schema creation, extraction-run creation, deprecated\n`verify()`, and webhook creation carry an `Idempotency-Key`. The client generates a key when you omit\none.\n\nA check reads current state. The server does not replay it, so a retry recomputes the result.\n\n## API origin env vars\n\nTwo names exist on purpose — they are **not** interchangeable:\n\n| Consumer                          | Variable         | Reads env?                    |\n| --------------------------------- | ---------------- | ----------------------------- |\n| Python SDK / MCP                  | `KAVAL_BASE_URL` | yes                           |\n| Node `@usekaval/kaval`            | —                | pass `baseUrl` in constructor |\n| Marketing site proxy (`apps/web`) | `KAVAL_API_URL`  | yes (server only)             |\n\nUse the same origin value in both vars when self-hosting (e.g. `http://localhost:8787`, the port the\nserver image listens on). The self-host guide ships with the server distribution rather than here —\nKaval's core repo is private, so there is no public link to give you.\n\n## Honest boundaries\n\nDemo results carry no organizational authority; a production `ALLOW` requires a customer-bound\naction policy and applicable empirical calibration; **`REVIEW` is never permission to act**.\n\n## Development\n\n```bash\npnpm install\npnpm check        # build + lint + typecheck + test (the JS packages)\npnpm check:docs   # this README and the CHANGELOG against the shipped surface\n\n# Python SDK\ncd sdks/python && pip install -e \".[dev]\" && pytest\n```\n\nEverything above is hermetic: it fakes the API, which is why a client that aborts its own headline\ncall, or an endpoint that 404s, can pass it. Two suites talk to a real server, and both skip\nthemselves when their credentials are absent:\n\n```bash\nexport KAVAL_API_KEY=kv_live_…\nexport KAVAL_BASE_URL=https://api.usekaval.com   # or http://localhost:8787\n\npnpm --filter @usekaval/mcp exec vitest run test/live-tools.test.ts\ncd sdks/python && pytest tests/test_live.py\n```\n\nThese need a running Kaval server and an issued API key, so they are opt-in and self-skip without\none. They are **not** a release gate here: this repository cannot reach the server's source, and\nthere is no staging deployment — the only deployment is production, and pointing a publish gate at\nit would write test sources, receipts and outcome reports into the live product on every tag.\n\nThe real client-vs-server contract test lives in the Kaval server repository's CI, where a real\nPostgres and the server both exist. It boots the server, issues a scoped key, and drives these same\nclients against it on every push. Publishing from this repository gates on the hermetic suites,\nwhich is what this repository can honestly verify on its own.\n\n## License\n\n[Apache-2.0](LICENSE).\n",
  "bytes": 20113,
  "sha": "3c3ad2ea81dc31e5539da695966675590ffd9e8a5c933a15eb1ebf0c60408189",
  "repo_slug": "lufemc/kaval-clients",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_lufemc_kaval_dee19c3c/readme"
}