Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this SDK are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[SemVer](https://semver.org/).

## [2.7.1] - 2026-08-15

### Fixed

- **`library.list` no longer documents the `popular` sort.** The server
retired the view counter and its popularity sort (Lenz #273) and silently
coerces `sort=popular` to `recent`, so the docstring advertised a dead
option. `sort` stays a plain `str`, so nothing breaks — a caller that still
sends `"popular"` keeps getting `recent` ordering from the server.
- Refreshed the `openapi.json` snapshot (doc-only server drift: /extract
enumeration semantics, /verify body-keyed idempotency, the errors table).

## [2.7.0] - 2026-08-10

Quota errors are now a first-class, typed condition instead of an
Expand Down
14 changes: 9 additions & 5 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "Lenz \u2014 AI Fact-Checking API",
"version": "1.0.0",
"description": "# Four API primitives for AI product teams\n\nA research-depth ladder: find claims, judge them fast, prove them deep, follow up.\n\n- `POST /extract` \u2014 pull verifiable claims out of any text. Free, 1000/key/day.\n- `POST /assess` \u2014 fast 3-model frontier panel verdict in ~5-10s. Sync, paid.\n- `POST /verify` \u2014 full 8-model pipeline with citations in ~90s. Async, paid.\n- `POST /ask/{id}` \u2014 ask follow-up questions grounded on a verification.\n\n## Built for teams whose AI output is async or document-shaped\n\nLegal-memo generators, AI deep-research, due-diligence platforms, vertical agents producing structured deliverables, \"report-as-a-service\" companies. `/assess` opens the door to sync UX too \u2014 fast enough to gate a chat completion or a UI submit.\n\n## Install the SDK\n\n```bash\npip install lenz-io # Python\nnpm install lenz-io # Node / TypeScript\n```\n\nSource on GitHub: [lenz-io-python](https://github.com/lenzhq/lenz-io-python) \u00b7 [lenz-io-node](https://github.com/lenzhq/lenz-io-node).\n\n## The canonical integration: extract \u2192 assess \u2192 conditional verify\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# 1. Pull factual claims out of your model output (free, ~1s)\nclaims = client.extract(text=llm_output).identified_claims\n\n# 2. Fast verdict on all of them via /assess (~5-10s, 3-model panel)\nquick = client.assess(text=llm_output)\n\n# 3. Escalate low-confidence claims to /verify (~90s, 8-model pipeline)\nfor c in quick.claims:\n if c.confidence == \"low\":\n deep = client.verify_and_wait(claim=c.claim).verdict\n print(deep.verdict, deep.lenz_score)\n```\n\n## How /verify works under the hood\n\nFrame \u2192 Collect Evidence \u2192 Debate (2 models, 2 rounds) \u2192 Adjudicate (3 models: sources, logic, precision) \u2192 Conclude. ~90 seconds wall-clock per claim.\n\n## Common patterns\n\n- **Runtime sync UX**: `POST /assess` on the user-facing path; escalate `confidence == \"low\"` claims to `/verify` in the background.\n- **Runtime webhook**: `POST /verify` with `webhook_url`. Lenz POSTs the typed payload when done. Best for document pipelines.\n- **CI / pre-release**: `/extract` + batch `/verify` your golden set on every deploy. The free `/extract` tier covers most of this.\n- **Incident triage**: paste the offending output into `/extract`, then `/verify` the wrong-looking claims. Get a citation trail to send back to the customer.\n\n## What's in the response\n\nUnified vocabulary across every claim-shaped response:\n\n- **`claim`** \u2014 the framed claim text.\n- **`verdict`** \u2014 `\"True\" | \"Mostly True\" | \"Mixed\" | \"Mostly False\" | \"False\" | \"Error\"`.\n- **`confidence`** \u2014 `\"high\" | \"medium\" | \"low\"`.\n- **`lenz_score`** \u2014 1\u201310 integer score (deep payloads).\n- **`sources`** \u2014 evidence with title, URL, snippet (`/verify` only).\n- **`audit`** \u2014 adjudication reasoning, debate transcript, panel agreement (`/verify` only).\n\n## How the API is organized\n\n- **Extract** \u2014 pull verifiable claims out of text.\n- **Assess** \u2014 fast 3-model verdict.\n- **Verify** \u2014 deep 8-model pipeline, submit and poll status.\n- **Ask** \u2014 follow-up questions on a verification.\n- **Verifications** \u2014 list, fetch, delete, and toggle visibility on stored verifications.\n- **Library** \u2014 browse the public catalog. No API key needed.\n- **Account** \u2014 credits and rate-limit usage.\n\n## Authentication\n\n1. [Create a Lenz account](https://lenz.io/auth).\n2. Generate an API key on the [API Integration](https://lenz.io/api-integration) page.\n3. Send it on every request:\n ```\n Authorization: Bearer lenz_...\n ```\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /extract`, `POST /assess`, `POST /verify`, and `POST /verify/batch` so retries after a network drop do not spawn duplicate tasks or double-debit quota. The server caches the response body and returns it on replay; a re-used key with a different body returns 422. The SDKs auto-generate a key per call by default.\n\n## Webhooks\n\nOn `POST /verify` you may supply a `webhook_url`. We POST an HMAC-SHA256-signed JSON payload to it when the pipeline terminates. Signature header is `X-Lenz-Signature: sha256=<hex>`. Verify with your webhook secret (generate + rotate on [API Integration](https://lenz.io/api-integration)). Retries: 10s, 60s, 600s (3 retries after the initial delivery, 4 attempts total). The SDKs ship a `LenzWebhooks` helper that verifies signatures and parses the payload into typed events.\n\nFor commercial use, volume pricing, or onboarding support, [get in touch](https://lenz.io/contact).",
"description": "# Four API primitives for AI product teams\n\nA research-depth ladder: find claims, judge them fast, prove them deep, follow up.\n\n- `POST /extract` \u2014 enumerate every major verifiable claim in a document (up to 25, most check-worthy first, inputs to 50k chars). Free, 1000/key/day.\n- `POST /assess` \u2014 fast 3-model frontier panel verdict in ~5-10s. Sync, paid.\n- `POST /verify` \u2014 full 8-model pipeline with citations in ~90s. Async, paid.\n- `POST /ask/{id}` \u2014 ask follow-up questions grounded on a verification.\n\n## Built for teams whose AI output is async or document-shaped\n\nLegal-memo generators, AI deep-research, due-diligence platforms, vertical agents producing structured deliverables, \"report-as-a-service\" companies. `/assess` opens the door to sync UX too \u2014 fast enough to gate a chat completion or a UI submit.\n\n## Install the SDK\n\n```bash\npip install lenz-io # Python\nnpm install lenz-io # Node / TypeScript\n```\n\nSource on GitHub: [lenz-io-python](https://github.com/lenzhq/lenz-io-python) \u00b7 [lenz-io-node](https://github.com/lenzhq/lenz-io-node).\n\n## The canonical integration: extract \u2192 assess \u2192 conditional verify\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# 1. Enumerate the factual claims in your model output (free)\n# identified_claims is the complete ordered list when >1 claim is\n# found (top claim included \u2014 iterate it alone); for a single claim\n# it is [] and the claim lives in .claim.\nextracted = client.extract(text=llm_output)\nclaims = extracted.identified_claims or [extracted.claim]\n\n# 2. Fast verdict on all of them via /assess (~5-10s, 3-model panel)\nquick = client.assess(text=llm_output)\n\n# 3. Escalate low-confidence claims to /verify (~90s, 8-model pipeline)\nfor c in quick.claims:\n if c.confidence == \"low\":\n deep = client.verify_and_wait(claim=c.claim)\n print(deep.verdict, deep.lenz_score, deep.key_finding)\n```\n\n## How /verify works under the hood\n\nFrame \u2192 Collect Evidence \u2192 Debate (2 models, 2 rounds) \u2192 Adjudicate (3 models: sources, logic, precision) \u2192 Conclude. ~90 seconds wall-clock per claim.\n\n### When /verify asks for input\n\n`/verify` is not only submit-and-poll. `GET /verify/status/{task_id}` can terminate at `needs_input` \u2014 the submitted text held several claims, or was too vague to check as written \u2014 and the task waits rather than guessing which claim you meant.\n\nResolve it with `POST /verify/{task_id}/select`, passing the exact wording of the claim(s) you want from the ones the status offered. Selection is **by text, not index**; anything that was not offered is rejected with a 422. Each selected claim fans out into its own pipeline, so the response carries one `task_id` per claim \u2014 poll each the way you would a fresh `/verify`.\n\nClaims returned by `/extract` skip this entirely: submitted verbatim, they are never bounced back for rephrasing.\n\n## Common patterns\n\n- **Runtime sync UX**: `POST /assess` on the user-facing path; escalate `confidence == \"low\"` claims to `/verify` in the background.\n- **Runtime webhook**: `POST /verify` with `webhook_url`. Lenz POSTs the typed payload when done. Best for document pipelines.\n- **CI / pre-release**: `/extract` + batch `/verify` your golden set on every deploy. The free `/extract` tier covers most of this. Claims returned by `/extract` verify cleanly \u2014 submitted verbatim to `/verify`, they are never bounced back for rephrasing.\n- **Incident triage**: paste the offending output into `/extract`, then `/verify` the wrong-looking claims. Get a citation trail to send back to the customer.\n\n## What's in the response\n\nUnified vocabulary across every claim-shaped response:\n\n- **`claim`** \u2014 the framed claim text.\n- **`verdict`** \u2014 `\"True\" | \"Mostly True\" | \"Mixed\" | \"Mostly False\" | \"False\" | \"Error\"`.\n- **`confidence`** \u2014 `\"high\" | \"medium\" | \"low\"`.\n- **`lenz_score`** \u2014 1\u201310 integer score (deep payloads).\n- **`key_finding`** \u2014 one declarative sentence stating the most important fact the analysis established. For a false claim it states the CORRECTED fact rather than restating the claim, so render it next to the verdict label, never as a standalone headline. `\"\"` on older verifications predating the field.\n- **`sources`** \u2014 evidence with title, URL, snippet (`/verify` only).\n- **`audit`** \u2014 adjudication reasoning, debate transcript, panel agreement (`/verify` only).\n\n## How the API is organized\n\n- **Extract** \u2014 pull verifiable claims out of text.\n- **Assess** \u2014 fast 3-model verdict.\n- **Verify** \u2014 deep 8-model pipeline: submit one claim or a batch, poll status, and resolve a `needs_input` interrupt by selecting claims.\n- **Ask** \u2014 follow-up questions on a verification.\n- **Verifications** \u2014 list, fetch, delete, and toggle visibility on stored verifications.\n- **Library** \u2014 browse the public catalog. No API key needed.\n- **Account** \u2014 credits and rate-limit usage.\n\n## Authentication\n\n1. [Create a Lenz account](https://lenz.io/auth).\n2. Generate an API key on the [API Integration](https://lenz.io/api-integration) page.\n3. Send it on every request:\n ```\n Authorization: Bearer lenz_...\n ```\n\nOr skip steps 1\u20132: **[lenz.io/setup](https://lenz.io/setup)** issues a test key and generates setup instructions for your environment \u2014 MCP client, SDK or plain HTTP \u2014 that you paste into your coding agent to do the wiring.\n\n### One balance, whichever door you come through\n\nLenz also runs a remote MCP server at `https://lenz.io/mcp`, so an agent can fact-check inside a conversation. It is **not** part of this surface \u2014 there are no MCP endpoints below, and its tools are documented at [lenz.io/integrations/mcp-server](https://lenz.io/integrations/mcp-server).\n\nIt matters here for one reason: that server is a client of this API. It takes the same `lenz_...` key and forwards it to the endpoints below, so MCP traffic spends the same credits, counts against the same rate limits, and shows up in `GET /me/usage` \u2014 there is no separate MCP quota to check, and a 402 can just as easily be an agent as your own code.\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /extract`, `POST /assess`, `POST /verify`, and `POST /verify/batch` so retries after a network drop do not spawn duplicate tasks or double-debit quota. The server caches the response body and returns it on replay; a re-used key with a different body returns 422. The SDKs auto-generate a key per call by default.\n\n## Errors\n\nThe status code says what to do next; the body says what happened. Every response carries an `X-Request-ID` header \u2014 quote it on a support ticket.\n\n| Status | Condition | Retry? |\n|---|---|---|\n| 401 | Missing, malformed or unknown key | After fixing credentials |\n| 402 | Out of credits, or the plan is the limit | No \u2014 billing action |\n| 403 | Authenticated but not allowed (private verification, IP block) | No |\n| 409 | An identical request is already in flight | Yes, shortly |\n| 422 | Malformed input | No |\n| 429 | Per-key rate limit (the `/extract` daily cap) | Yes, after `Retry-After` |\n\n**402** carries `detail`, `code` (always `no_credits`), `upgrade_url`, `doc_url`, `wall_id`, and \u2014 when resolvable \u2014 `remaining` and `resets_at`. Those two are omitted rather than sent as `null`, so an absent key means \"unknown\", not \"zero\".\n\n`wall_id` identifies this specific rejection and is already appended to `upgrade_url`, so sending a user to that URL is all that is needed \u2014 it exists as its own field only for clients that build their own upgrade link. Treat `upgrade_url` as opaque and follow it whole rather than matching it against a fixed string.\n\nThe `code` values are deliberately plan-agnostic: they name the condition, never the plan that would fix it. Send users to `upgrade_url` rather than mapping codes to plan names \u2014 the set of plans changes, the conditions do not.\n\nQuota is deliberately **not** a 429: a 429 tells every client to retry, and an empty balance never succeeds on retry. **429** carries `code`, `limit`, `reset_in_seconds`, `upgrade_url` and the `Retry-After` header.\n\n**Migrating (August 2026).** These rejections were 403, which the SDKs map to their auth error; they are now 402, which maps to the quota error \u2014 not a subclass. If you catch the auth error to handle an empty balance, that branch stops firing. This reaches you on every SDK version, including ones released before the change. `detail` strings are unchanged. Full reference: [lenz.io/docs/errors](https://lenz.io/docs/errors#quota).\n\n## Webhooks\n\nOn `POST /verify` you may supply a `webhook_url`. We POST an HMAC-SHA256-signed JSON payload to it when the pipeline terminates. Signature header is `X-Lenz-Signature: sha256=<hex>`. Verify with your webhook secret (generate + rotate on [API Integration](https://lenz.io/api-integration)). Retries: 10s, 60s, 600s (3 retries after the initial delivery, 4 attempts total). The SDKs ship a `LenzWebhooks` helper that verifies signatures and parses the payload into typed events.\n\nFor commercial use, volume pricing, or onboarding support, [get in touch](https://lenz.io/contact).",
"termsOfService": "https://lenz.io/api-terms"
},
"paths": {
Expand Down Expand Up @@ -111,7 +111,7 @@
}
}
},
"description": "Search and browse the public catalog of verified claims.\n\nReturns paginated results from the Lenz Library. Only publicly\npublished verifications are included.\n\n**Sort options:** `recent`, `popular`, `most_true`, `most_untrue`,\n`relevance` (only when `search` is provided), and `random`.\n\n**`curated`** restricts results to one or more named curated collections,\ncomma-separated. Currently supported: `trivia` (the LLM-curated,\ntrivia-worthy pool behind the open-source quiz demo).\n\n**`verdict`** filters to one or more verdict labels, comma-separated\n(e.g. `True,False`). Labels: `True`, `Mostly True`, `Mixed`,\n`Mostly False`, `False`.",
"description": "Search and browse the public catalog of verified claims.\n\nReturns paginated results from the Lenz Library. Only publicly\npublished verifications are included.\n\n**Sort options:** `recent`, `most_true`, `most_untrue`,\n`relevance` (only when `search` is provided), and `random`.\n\n**`curated`** restricts results to one or more named curated collections,\ncomma-separated. Currently supported: `trivia` (the LLM-curated,\ntrivia-worthy pool behind the open-source quiz demo).\n\n**`verdict`** filters to one or more verdict labels, comma-separated\n(e.g. `True,False`). Labels: `True`, `Mostly True`, `Mixed`,\n`Mostly False`, `False`.",
"tags": [
"Library"
],
Expand All @@ -137,7 +137,7 @@
"description": "OK"
}
},
"description": "Submit a claim for verification.\n\nReturns a ``task_id`` immediately. Poll ``GET /verify/status/{task_id}``\nor supply ``webhook_url`` in the body for an asynchronous callback when\nthe pipeline terminates.",
"description": "Submit a claim for verification.\n\nReturns a ``task_id`` immediately. Poll ``GET /verify/status/{task_id}``\nor supply ``webhook_url`` in the body for an asynchronous callback when\nthe pipeline terminates.\n\n**Duplicate submissions are collapsed.** With no ``Idempotency-Key``\nheader, the request body itself is the key: resubmitting an identical\nbody within 10 minutes returns the original ``task_id`` instead of\nstarting a second run. To run the same text twice on purpose, send your\nown ``Idempotency-Key`` \u2014 a supplied key always wins.",
"tags": [
"Verify"
],
Expand Down Expand Up @@ -267,7 +267,7 @@
"description": "OK"
}
},
"description": "Extract verifiable claims from arbitrary text.\n\nRuns only the framing step of the pipeline \u2014 no research, no debate,\nno credit charge. Returns the list of verifiable claims, candidate\ndisambiguations (if the input was ambiguous), domain, key entities,\nand presumed intent.\n\nFree for API-key holders, capped at 1000 calls per key per day\n(resets at 00:00 UTC). Keys with admin-set ``unlimited_extract=True``\nbypass the cap.",
"description": "Extract verifiable claims from arbitrary text.\n\nRuns the standalone extraction step (`lenz.extraction`) \u2014 no research,\nno debate, no credit charge. Enumerates every distinct major factual\nclaim in the input (up to 25), ordered most-check-worthy-first.\n`identified_claims` is the complete ordered list when more than one\nclaim is found \u2014 iterate it alone; don't concatenate it with `claim`\n(the top claim appears in both). Claims returned here verify cleanly\nwhen submitted verbatim to `POST /verify`.\n\nAccepts up to 50,000 characters (longer input is truncated). Free for\nAPI-key holders, capped at 1000 calls per key per day (resets at 00:00\nUTC) regardless of input length. Keys with admin-set\n``unlimited_extract=True`` bypass the cap.",
"tags": [
"Extract"
],
Expand Down Expand Up @@ -1705,7 +1705,7 @@
},
{
"name": "Verify",
"description": "Deep 8-model pipeline with citations. Async, ~90s. Submit + poll status."
"description": "Deep 8-model pipeline with citations. Async, ~90s. Submit one claim or a batch, poll status, and resolve a needs_input interrupt with select."
},
{
"name": "Ask",
Expand All @@ -1722,6 +1722,10 @@
{
"name": "Account",
"description": "Subscription credits and rate-limit usage."
},
{
"name": "Discovery",
"description": "Machine-readable index of the API root."
}
],
"webhooks": {
Expand Down
4 changes: 2 additions & 2 deletions src/lenz_io/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ def list(
``curated`` restricts to one or more named curated collections, e.g.
``["trivia"]`` (the pool behind the open-source quiz demo). ``verdict``
filters by comma-separated labels, e.g. ``"True,False"``. ``sort`` also
accepts ``"random"`` alongside ``recent`` / ``popular`` / ``most_true``
/ ``most_untrue`` / ``relevance``.
accepts ``"random"`` alongside ``recent`` / ``most_true`` /
``most_untrue`` / ``relevance``.
"""
params: dict[str, Any] = {
"page": page,
Expand Down
Loading