Errors reference
Every Lenz SDK error carries a one-line message, a
cause (why it fired), a fix (what to try), and
a doc_url pointing back here. From SDK 2.7.0
errors also carry the server's code, and specific errors add
their own fields — quota carries upgrade_url and
remaining, rate limits carry retry_after. This
page is the canonical list.
The HTTP status says what to do next, before anything parses the body:
| Status | Condition | Retry? |
|---|---|---|
| 401 | Missing, malformed or unknown key | After fixing credentials |
| 402 | Out of balance | No — billing action |
| 403 | Authenticated but not allowed (private verification, IP block) | No |
| 409 | An identical request is already in flight (in_progress), or the selection you're resolving was already resolved (no_selection_pending) | Only in_progress |
| 422 | Malformed input | No |
| 429 | Per-key rate limit | Yes, after Retry-After |
Every response also carries an X-Request-ID header. Include
it when you report a problem.
Authentication #
LenzAuthError
Fires when an API call needs a key but none was provided, or the key was rejected by the server. Causes: env var unset, wrong key, key revoked.
Fix: pass api_key= / apiKey: to
new Lenz(), set the LENZ_API_KEY env var,
or get one at lenz.io/api-integration.
The library endpoints don't need a key — every other verb does.
Verification #
LenzNeedsInputError
The pipeline paused for clarification. Two reasons:
| reason | what to do |
|---|---|
multi_claim |
The submitted text contains multiple distinct claims. The
error's claims array lists them. Resolve by calling
client.select(task_id, claim_index=0) with the
index you want, then re-call verify_and_wait()
on the returned task. |
clarification_required |
The submission is ambiguous. The error's
candidates array lists possible interpretations.
Pick one via client.select(task_id, text="..."). |
The duplicate_found branch is web-only — API customers
never see it (every API submission gets its own verification id; see
API Terms).
LenzPipelineError
Terminal failure inside the verification pipeline.
cause + failure_reason on the exception
describe the step that gave up.
Common reasons: framing_failed (the input wasn't a
verifiable claim), research_insufficient (couldn't
gather enough sources), conclusion_failed (the model
refused to commit). Retry with a different claim, or rephrase to
something more specific.
LenzTimeoutError
Your client-side polling timeout elapsed. The verification is
still running server-side — the exception's task_id
lets you resume:
Bump timeout= on verify_and_wait() if you
regularly hit this (default is 120s; typical pipeline is 60-90s).
Quota #
LenzQuotaExceededError (HTTP 402)
You're out of balance. Either the key's monthly quota and any bonus credits are both spent, or the capability needs a higher plan than the key's. Retrying will not clear it — the fix is a billing action.
The body:
| Field | Meaning |
|---|---|
code | Currently always no_credits — the 402 status is the signal, so you don't need to read this. It exists so a future condition can be added without a new status, and it names the condition, never the plan, capability, or endpoint. To tell "top up" from "retry a smaller batch", read remaining. |
upgrade_url | Where the wall lifts. Send users here rather than mapping codes to plan names. Also present on 429. Treat it as opaque — follow it whole rather than matching it against a fixed string; it carries a query parameter. |
wall_id | Identifies this specific rejection. Already appended to upgrade_url, so following that URL is all you need — this field exists for clients that build their own upgrade link and want the same continuity. Safe to log; it identifies the rejection, not the user. |
remaining | Usable capacity left for that capability (monthly quota + bonus credits). Omitted, never null, when the server can't resolve it — an absent key is honest; null would read as a zero balance. |
resets_at | When the monthly quota rolls over. Same omission rule. |
requested | Batch size that didn't fit. Only on /verify/batch, /verify/{id}/select and multi-claim /assess. |
Check remaining before you hit the wall via client.usage() —
monthly quota and one-off bonus credits are reported separately per
capability:
Re-submitting the same claim text under the same key after it
first succeeded is a same-user cache hit — returns the existing
verification_id for free, without burning a credit.
LenzAuthError. They
are now 402, which maps to LenzQuotaExceededError — and that
is not a subclass of LenzAuthError. If you
catch LenzAuthError to handle an empty balance, that branch
stops firing. Add a LenzQuotaExceededError handler.
This takes effect on every SDK version, including ones released before
the change — you don't need to upgrade for it to reach you.
detail strings are unchanged, so message-matching keeps
working.
Rate limits #
LenzRateLimitError (HTTP 429)
A per-key rate limit. Today that means the /extract daily
cap of 1000 calls per key, which resets at 00:00 UTC. Unlike a 402, this
one does clear on its own — retry after retry_after seconds.
The same value is on the Retry-After header. The SDKs retry
it automatically up to 3 times. From 2.7.0 they do so
only when the wait is 60 seconds or less, and above that raise
immediately with the true retry_after, so a call can't block
for hours inside a sleeping retry ladder — schedule the work instead.
Earlier versions sleep the full stated wait, which the daily cap can put
hours out. A paid plan lifts the cap.
Webhooks #
Every webhook POST carries an X-Lenz-Signature header
(sha256=...). Verify it before trusting the body.
The Python and Node SDKs both ship a webhook helper:
LenzWebhookSignatureError
The signature didn't match. Causes:
- Wrong secret. Compare against the value at /api-integration (Webhooks panel).
- Body modified in transit. Use the raw bytes — re-serializing the parsed JSON changes whitespace and breaks the HMAC.
- Replay older than the 5-minute window.
X-Lenz-Timestampguards against replay attacks.
LenzWebhookValidationError
The body parsed but didn't match the expected event schema.
Check that the SDK version is current (pip install -U lenz-io
/ npm install lenz-io@latest) — schema additions ship
as minor bumps.
verification.completed with the customer's own
verification_id — never another customer's. A same-user
cache-hit re-uses the existing claim's id; a different-user cache-hit
clones the verdict and fires with the clone's new id. Customers can
dedup on verification_id + event.
Validation #
LenzValidationError (HTTP 422)
The request body was malformed and no amount of retrying changes that.
Common causes: empty text, an empty claims or
texts array, a source_url without an
http:// or https:// scheme, a message over
500 characters on /ask, or an unsupported
language code.
A malformed count carries code=invalid_count. It is a 422
and never a 402 — a bad n is a bug in the call, not an
empty wallet, and sending someone to a pricing page over it wastes
their time.
Generic shapes #
LenzAPIError
Server returned a non-2xx that doesn't map to a more specific
exception. Inspect status_code, cause,
and request_id (forward this to support if you need
help triaging).
LenzError
Base class. Catch this if you want to handle every Lenz exception uniformly:
request_id from the exception. We page on
P1s and read every issue.
Start here if you're new: Quickstart →