Citecheck
For each citation, Lenz answers one question: does the cited source say what the text attributes to it? Each answer comes with the passage from the source it rests on, or says why the source could not be read. Each checked citation costs 1 credit: at most 1 credit per citation you send; a citation that could not be checked is refunded.
Two ways in. POST /citecheck is the building block: send pairs of a
statement and its link or DOI, or a whole text. The same check also runs inside
/review (below). The endpoint reference is at
/api/v1/docs; this page is the guide.
POST /citecheck #
POST /citecheck → GET /citechecks/{citecheck_id}. The first
call, two pairs of a statement and the page it cites:
curl -X POST https://lenz.io/api/v1/citecheck \
-H "Authorization: Bearer $LENZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pairs": [
{ "statement": "The Eiffel Tower was completed in 1889.",
"url": "https://en.wikipedia.org/wiki/Eiffel_Tower" },
{ "statement": "Mount Everest is 7,000 metres high.",
"url": "https://en.wikipedia.org/wiki/Mount_Everest" }
]
}'
# 202 {"citecheck_id": "217c8a01", "status": "queued"}
# Location: /api/v1/citechecks/217c8a01
Read the check at the Location until status is
completed or failed, honouring poll_after_seconds. This
is the response that call returned, recorded:
{
"citecheck_id": "217c8a01",
"status": "completed",
"outcome": "issues_found",
"created_at": "2026-09-27T19:24:05.738319Z",
"completed_at": "2026-09-27T19:24:08.838145Z",
"poll_after_seconds": null,
"policy": {
"max_citations": 2
},
"summary": {
"citations_found": 2,
"citations_selected": 2,
"citation_limit": 2,
"citation_limit_reached": false,
"citation_checks": {
"checked": 2,
"unchecked": 0,
"failed": 0
},
"citation_issues": 1
},
"credits": {
"charged": 2
},
"citations": [
{
"index": 0,
"reference": "https://en.wikipedia.org/wiki/Eiffel_Tower",
"cited_url": "https://en.wikipedia.org/wiki/Eiffel_Tower",
"doi": null,
"statement": "The Eiffel Tower was completed in 1889.",
"quotes": [],
"position": null,
"result": {
"finding": "supported",
"source": "support",
"is_issue": false
},
"check": {
"status": "completed",
"page_read": "full",
"page_title": "Eiffel Tower - Wikipedia",
"page_published_date": "2001-03-01",
"page_language": "en",
"source_url": "https://en.wikipedia.org/wiki/Eiffel_Tower",
"source_version": null,
"support": "supported",
"snippet": "Completed | 31 March 1889; 137 years ago [1]",
"rationale": "The source supports the statement, showing in the infobox that the Eiffel Tower was completed on 31 March 1889.",
"quote": null,
"doi_registered": null,
"metadata": null,
"metadata_differences": [],
"registered": null,
"unchecked_reason": null,
"hint": null,
"failure": null,
"missing_quote": null
}
},
{
"index": 1,
"reference": "https://en.wikipedia.org/wiki/Mount_Everest",
"cited_url": "https://en.wikipedia.org/wiki/Mount_Everest",
"doi": null,
"statement": "Mount Everest is 7,000 metres high.",
"quotes": [],
"position": null,
"result": {
"finding": "contradicted",
"source": "support",
"is_issue": true
},
"check": {
"status": "completed",
"page_read": "partial",
"page_title": "Mount Everest - Wikipedia",
"page_published_date": "2002-02-28",
"page_language": "en",
"source_url": "https://en.wikipedia.org/wiki/Mount_Everest",
"source_version": null,
"support": "contradicted",
"snippet": "Its height was most recently measured in 2020 through a joint survey by Nepalese and Chinese authorities as 8,848.86 m (29,031 ft 81⁄2 in).",
"rationale": "The source states that Mount Everest's height was measured in 2020 as 8,848.86 metres, which contradicts the statement's claim of 7,000 metres.",
"quote": null,
"doi_registered": null,
"metadata": null,
"metadata_differences": [],
"registered": null,
"unchecked_reason": null,
"hint": null,
"failure": null,
"missing_quote": null
}
}
],
"citation_issues": [
{
"citation_index": 1,
"reference": "https://en.wikipedia.org/wiki/Mount_Everest",
"cited_url": "https://en.wikipedia.org/wiki/Mount_Everest",
"doi": null,
"statement": "Mount Everest is 7,000 metres high.",
"quotes": [],
"position": null,
"finding": "contradicted",
"source": "support",
"snippet": "Its height was most recently measured in 2020 through a joint survey by Nepalese and Chinese authorities as 8,848.86 m (29,031 ft 81⁄2 in).",
"rationale": "The source states that Mount Everest's height was measured in 2020 as 8,848.86 metres, which contradicts the statement's claim of 7,000 metres.",
"metadata_differences": [],
"missing_quote": null,
"page_title": "Mount Everest - Wikipedia",
"failure": null
}
],
"citation_failures": [],
"failure": null,
"more_citations": []
}
The rows, the findings and the counts are described below, and are
the same inside /review.
Pairs
pairs holds 1 to 20 items. You say what each source is cited
for, so no pairing step runs, and position is null. Every pair is
checked: policy.max_citations is the number of pairs, and sending
max_citations with pairs is a 422.
| Field of an item | Meaning |
|---|---|
statement | Required. What the source is cited for, up to 1,000 characters. |
url or doi | Exactly one. A public http(s) address, or a whole DOI such as 10.1038/nature12373. |
quotes | Optional, up to 3: words of the statement the source is said to have written. Each must appear in the statement. |
cited_title, cited_authors, cited_year, cited_journal | Optional, with a doi only: what your reference says, compared with the registry's record. |
A whole text
Send text instead of pairs (exactly one of the two), and Lenz finds
the citations, pairs each with the sentence it supports and checks the first
max_citations of them (1 to 20, 20 when
omitted). Links are read as below. The citations found past those
are listed, unchecked, in more_citations (up to 100; [] for pairs),
so you can check the rest in a later request. A text with no
citation fails as no_citations, and a text that is one URL is refused with
url_input.
curl -X POST https://lenz.io/api/v1/citecheck \
-H "Authorization: Bearer $LENZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "…the draft, with its links…", "max_citations": 20 }'
Both forms take language, the language Lenz writes the reasoning in
(rationale), English when omitted. Hints are always in English, and the
snippet and quotes stay verbatim in the page's language. The draft's own
language is detected, never asked for. They also take
webhook_url and an Idempotency-Key header, as /review
does. A check needs at least
1 credit on the account to be accepted.
Reading a check
GET /citechecks/{citecheck_id} answers the check as it stands, never cached.
status moves through queued, checking, then
completed or failed. An unknown id is 404 not_found,
and a check whose content your account's retention has removed is 410
purged. Through OAuth, a check made by another connection needs the
history:read scope.
| When | status | outcome |
|---|---|---|
| an issue found, nothing failed | completed | issues_found |
| at least one citation checked, no issue, nothing failed | completed | clean |
every citation unchecked | completed | unchecked |
| a check failed on our side, or ran out of time | completed | incomplete |
a text with no citation (no_citations), or the pairing step unavailable (upstream_unavailable) | failed | unchecked |
Webhooks
With webhook_url, Lenz posts citecheck.completed or
citecheck.failed, carrying citecheck_id and the check's body, signed
and delivered like the review's events (webhooks).
Errors
| HTTP | code | When | What to do |
|---|---|---|---|
| 422 | validation_error | neither or both of text and pairs, max_citations with pairs, or a field out of range | The error names the field and its limit. |
| 422 | url_input | text is one URL | Send the page's text, with its links, to check the sources it cites. |
| 422 | invalid_doi | a doi that is not a whole DOI | Send the DOI alone, like 10.1038/nature12373. |
| 422 | quote_not_in_statement | a quote its statement does not contain | Each quote must be words of its statement. |
| 422 | webhook_secret_missing | a webhook_url on a credential with no signing secret | Create one on the API credentials page, or read GET /me/webhook-secret for an OAuth connection. |
| 422 | idempotency_body_mismatch | an Idempotency-Key reused with a different body | Use a new key for a new request. |
| 402 | no_credits | under 1 credit at acceptance | Top up, or wait for the monthly allowance. |
| 409 | idempotency_conflict | a check with this Idempotency-Key is still being created | Retry shortly; citecheck_id names that check once it exists. |
| 429 | citecheck_in_flight | three checks already running on the account | Wait for one to finish; honour Retry-After. |
| 503 | capacity | the services that read or judge pages are saturated | Retry after Retry-After. Nothing was charged. |
| 503 | citations_unavailable | citation checking is switched off right now | Retry after Retry-After. Nothing was charged. |
A check that fails after it was accepted carries the usual failure block
(failure_reason, failure_class, retryable,
hint), with the reasons no_citations (the text has no link, DOI or
numbered reference with one), upstream_unavailable (retryable) and
insufficient_credits (the balance did not cover the citations selected when the
check started; nothing was charged).
How to send links #
For a whole text (in /citecheck or /review) there is no
separate field for links. They are read from text, up to 50,000 characters,
and up to 20 of them are checked per request (max_citations).
These forms are read:
- Markdown links:
[a report from the ministry](https://example.gov/report-2024). The words are whatreferencereturns. - Bare URLs, and URLs in angle, round or square brackets:
https://example.org/study,<https://example.org/study>,(https://example.org/study),[https://example.org/study]. The brackets and any trailing punctuation are trimmed. - Addresses that start with
www.and have no scheme:www.example.org/study, read ashttps://. - DOIs, as
doi:10.1038/nature12373or as a link,https://doi.org/10.1038/nature12373. A DOI with neither marker is read too, but when it does not resolve it is reported asunchecked(ambiguous_reference) rather than as a DOI that does not exist. - Numbered markers with a reference list:
[1],[1, 2],[1-3]or a footnote[^a]in the body, and the entries below it as[1] …,[1]: …,1. …or[^a]: …. Each marker is one use of its entry; an entry with a URL or a DOI that no marker names is counted too.
Not read as citations:
- A reference with no URL and no DOI (Smith et al., 2021).
- A bare domain without
www.(example.org/study): it cannot be told apart from ordinary text. - A DOI inside a publisher's URL: that URL is checked as a web page.
- A
textthat is one URL. The page is read for its claims, but a page's text arrives without its links, sosummary.citations_skippedreadsurl_input.
From HTML
In a word processor or a CMS a link sits behind words, and plain text drops it. Send the
HTML converted to Markdown, which keeps every link as [words](url). Any
converter works. In Python, with markdownify:
from markdownify import markdownify text = markdownify(html)
In JavaScript, with turndown:
import TurndownService from "turndown"; const text = new TurndownService().turndown(html);
A Markdown file needs no conversion.
Findings #
Every row carries one finding in result.finding, and every row of
citation_issues[] carries it as finding. When more than one applies,
the row shows the first in this order. source names the check the finding came
from.
finding | What it means | What to do | Issue |
|---|---|---|---|
doi_not_found | The DOI is not registered with any DOI registry. Only reported for a DOI written as doi: or as a doi.org link and read whole. | Check the DOI against the paper the writer meant, and correct it. | yes |
page_not_found | Every provider that reached the link got "not found" (404 or 410). | Ask for the working address, or an archived copy of the page. | yes |
contradicted | The source says something else. snippet is the passage. | Compare the snippet with the draft's sentence, then correct the sentence or replace the link. | yes |
quote_not_in_source | Words the draft puts in quotation marks and attributes to this source are not in the page Lenz read in full. missing_quote is the excerpt that was not found. | Check the quote against the source. It may be worded differently there, or come from another article. | yes |
not_in_source | The source was read in full and says nothing on the sentence. | Ask which passage supports the sentence, or find a source that does. | yes |
metadata_mismatch | For a DOI: the reference's year, first author or title differs from the registry's record. metadata_differences lists each, registered holds the record. | Correct the reference, or check that the DOI is the paper the writer meant. | yes |
partly_supported | The source says part of it, or says it with a qualifier the draft dropped ("up to", "in one study"). snippet is the passage. | Narrow the sentence to what the source says, or restore the qualifier. | no |
supported | The source says it. snippet is the passage. | Nothing. | no |
unchecked | The citation could not be checked. check.unchecked_reason says why and check.hint what to do. | Check it by hand if it matters; see the reasons. | no |
An absence is only reported on a source read in full. not_in_source,
quote_not_in_source and partly_supported need the whole text: on a
page cut short, a teaser or an abstract alone, a passage that is missing proves nothing, and
the row is unchecked with the reason partial_text.
partly_supported is reported but is not an issue: the source backs part of the
sentence, so someone should compare the two before it ships. The row keeps its
snippet and rationale, with is_issue false. It is not
in citation_issues[], not counted in summary.citation_issues, and
does not make outcome issues_found on its own. Until September
2026 it was an issue. result is derived when the body is read, so a check made
before the change reads by the current rule.
A finding is about the citation's statement. With pairs, that is the statement
you sent. With a text, a model pairs each citation with its sentence, so read
statement before acting on a finding: when a link was paired with the wrong
sentence, the finding is about that sentence.
unchecked is not a failure #
unchecked means Lenz could not give an answer for a reason of the page or the
draft. It is not an issue, it does not change outcome, and it is counted in
citation_checks.unchecked. A source that gave no text reads the same whether it
is behind a subscription, a cookie wall or anything else: Lenz does not tell these apart,
and does not report which.
unchecked_reason | Meaning |
|---|---|
no_text | The source gave no text that could be read. |
partial_text | Only part of the source could be read, so what it leaves out cannot be judged. |
login_required | The site shows its pages only after a login (Facebook, Instagram, Threads, LinkedIn). |
unsupported_site | Links of this kind are not read. Video links are among them. |
no_statement | No sentence of the draft could be paired with the citation (a "Further reading" link, an entry no marker names). A DOI on such a row still gets its registry and metadata checks. |
unclear_pairing | Lenz could not tell which claim of the text the citation is given for, so it did not check it against one. Check it by hand, or send it as a pair with the statement it supports. |
invalid_url | The link is not a public http(s) address. It is not sent to a provider. |
other_version | For a DOI, only a preprint or an accepted manuscript could be read. A supported finding stands; an issue found by the support or the quote check becomes unchecked instead, because the published wording may differ. check.source_version names the version read. |
inconclusive | The source was read and does not settle the sentence either way. |
ambiguous_reference | The DOI could not be read off the text with certainty. Write it as doi:10.… or as a doi.org link. |
The list can grow: treat an unchecked_reason you do not know as "could not be
checked", and show its hint.
Failed checks
A check that ended with no finding for a reason on our side (a provider outage, the time
limit) has check.status: "failed" and a row in citation_failures[],
with the usual failure block. failure.retryable says whether to send the request
again. The citation checks of one request share a time limit of 120
seconds; a check still running then fails with timeout. A finding a row had
already established is kept: such a row stays in citation_issues[] with its
failure set, and is not in citation_failures[].
A row #
citations[] holds one row per citation checked, in order. The finding is in
result; what each check established is in check. A reference with a
DOI and the wrong year:
{
"index": 4,
"reference": "Kucsko G. et al. Nanometre-scale thermometry in a living cell. Nature (2015). doi:10.1038/nature12373",
"cited_url": "https://doi.org/10.1038/nature12373",
"doi": "10.1038/nature12373",
"statement": "Diamond sensors can measure temperature inside a living cell [5].",
"quotes": [],
"position": { "start": 2210, "end": 2275, "text": null },
"result": { "finding": "metadata_mismatch", "source": "metadata", "is_issue": true },
"check": {
"status": "completed",
"page_read": "full",
"page_title": "Nanometre-scale thermometry in a living cell",
"page_published_date": "2013-07-31",
"page_language": "en",
"source_url": "https://www.nature.com/articles/nature12373",
"source_version": "published",
"support": "supported",
"snippet": "we demonstrate a new approach to nanoscale thermometry that uses coherent manipulation of the electronic spin associated with nitrogen-vacancy colour centres in diamond",
"rationale": "The paper reports temperature measurement in living cells with diamond sensors.",
"quote": null,
"missing_quote": null,
"doi_registered": true,
"metadata": "mismatch",
"metadata_differences": [ { "field": "year", "cited": "2015", "registered": "2013" } ],
"registered": { "title": "Nanometre-scale thermometry in a living cell", "authors": ["Kucsko", "Maurer", "Yao", "Kubo"], "year": 2013, "journal": "Nature" },
"unchecked_reason": null,
"hint": null,
"failure": null
}
}
Field of check | Meaning |
|---|---|
status | Progress: pending, running, completed, failed. result is null until the check has ended. |
page_read | What reading the source gave: full, partial (cut short, a teaser or an abstract alone), not_found, none. |
source_url | The address the text was read from: the final address after redirects, or the free copy of a paper. |
source_version | For a DOI, the version read: published, accepted or submitted. null for a plain link. |
support | The support check: supported, partly_supported, contradicted, not_in_source, unchecked. |
snippet | The passage from the source the support answer rests on, checked against the page text. |
rationale | A reviewer's one-sentence reasoning. Not a checked source. |
quote | The quote check, when the draft quotes this source: matched, not_in_source, unchecked. null when it quotes nothing. It checks that the words are in the source, not who said them. |
missing_quote | The quoted excerpt that was not found in the source. null unless quote is not_in_source. The issue row carries the same key. |
doi_registered, metadata, metadata_differences, registered | The DOI checks: whether the DOI is registered, whether the reference's details agree with the registry's record, and the record itself. null with no DOI. |
unchecked_reason, hint | On an unchecked row: why, and one sentence on what to do. |
A DOI is checked three ways: the DOI registry says whether it exists; Crossref or OpenAlex
gives the record the reference is compared with (year, first author, title); and the text is
read from the publisher's page or from a free copy of the paper, with the abstract as a last
resort. A metadata mismatch is reported only when the difference is clear: a year off by one
is unchecked, since online and print dates often differ by a year.
The issue row
citation_issues[] holds the citations whose source does not hold up, the most
serious first, then in order:
"citation_issues": [
{
"citation_index": 0,
"reference": "a report from the ministry",
"cited_url": "https://example.gov/report-2024",
"doi": null,
"statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.",
"quotes": [],
"position": { "start": 0, "end": 118, "text": null },
"finding": "contradicted",
"source": "support",
"snippet": "The unemployment rate averaged 4.6% in 2024, down from 4.9%.",
"rationale": "The report gives 4.6% for 2024; the draft says 4.1%.",
"missing_quote": null,
"metadata_differences": [],
"page_title": "Labour market report 2024",
"failure": null
}
]
statement is what the citation is cited for, with link syntax reduced to its
words. For a text, position is where that sentence sits in the text you sent:
start and end in Unicode code points, link syntax included, so you can
mark it in your editor without searching for the string. It is the same shape as a claim's
positions (below), with text always null
because the row carries the statement. For pairs position is null. snippet and rationale are set
only when source is support; on metadata_mismatch,
metadata_differences says what differs.
snippet is copied from the source and checked against the page text Lenz read.
rationale is a reviewer's reasoning, not a checked source: read it as the reason
for the finding, and read the snippet as the evidence.
outcome #
A failed check makes outcome incomplete, which comes before an
issue; otherwise an issue makes it issues_found. unchecked and
partly_supported rows never change it on their own. For /citecheck, the table under
reading a check is the whole rule. Inside /review, the
citations join the claims in the same four values: issues_found with an empty
issues[] means the rows are in citation_issues[], and a review that
checks no citation computes outcome exactly as before.
In /review #
A /review call checks the draft's citations when you set
escalate.max_citations, from 1 to 20, and the review checks up
to that many citations, in the order they appear in the draft; 0, the default,
checks none. The claims are reviewed and charged as usual. Links are read from
text, as above.
curl -X POST https://lenz.io/api/v1/review \
-H "Authorization: Bearer $LENZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Unemployment fell to 4.1% in 2024, according to [a report from the ministry](https://example.gov/report-2024). ...",
"escalate": { "max_citations": 20 }
}'
The review body adds citations[] (in the full view, beside
claims[]), citation_issues[] and citation_failures[]
(in both views), the counts in summary, and the lists of what was found and not
checked. While citation checks are still running after the claims are done,
status reads verifying. review.completed carries the
same body.
The counts
summary says how many citations the draft has and how many were checked, so a
reader knows when only some were. A /citecheck body carries the same
counts, without citations_skipped:
"summary": {
"citations_found": 23,
"citations_selected": 20,
"citation_limit": 20,
"citation_limit_reached": true,
"citation_checks": { "checked": 16, "unchecked": 4, "failed": 0 },
"citation_issues": 3,
"citations_skipped": null
}
| Field | Meaning |
|---|---|
citations_found | Every use of a URL or a DOI in the text read (for pairs, the number of pairs). Exact. The same link cited twice counts twice. null until the draft is read, or when the check was not asked for. |
citations_selected | The ones this review checks: the first ones, in the draft's order. |
citation_limit / citation_limit_reached | The resolved cap, and whether the draft has more citations than it. |
citation_checks | Three counts that do not overlap: checked (any finding other than unchecked), unchecked (the page or the draft did not allow a check), failed (no finding, for a reason on our side). Once every check has ended they add up to citations_selected. |
citation_issues | The number of rows in citation_issues[]. |
citations_skipped | Why the check was asked for and did not run: url_input (the text was one URL), insufficient_credits (the balance did not cover the citations selected, one credit each; the claims are still reviewed and nothing is charged for citations, so top up and send the review again), or switched_off. Otherwise null. Read it before trusting a clean outcome: a skipped check has checked no citation. |
The draft is cut at 50,000 characters, as for every review. When
summary.input_truncated is true, more citations may exist past the cut.
What was found and not checked
Two lists in the body hold what the review found beyond what it checked. Both are always
present: null until the draft is read, [] when there is nothing
more. They cost nothing, since nothing in them was checked.
| Field | Meaning |
|---|---|
more_citations | The citations found beyond the ones checked, up to 100, in the draft's order: index, reference, cited_url, doi, sentence, position. |
more_claims | The claims found beyond max_assessments, as plain strings, up to 20 in all. |
more_claim_locations | Where the draft makes each of more_claims, in the same order: {claim, positions}, shaped like a claim row's positions (below). |
sentence is the sentence of the draft around the citation, cut by code with no
model. It differs from a checked row's statement, which is what the citation
was found to be cited for. Both are in the text with link syntax reduced to its words;
position points into the text as you sent it.
Where the draft makes each claim
A review checks only the claims it can trace directly back to the draft: a claim found nowhere
in the draft is left out and never charged. Each claim row carries positions, every
place the draft makes that claim, in text order, at most 10. Claim and citation positions share
one shape:
| Field | Meaning |
|---|---|
start, end | Where the passage sits in the text you sent, in Unicode code points, end exclusive. null when the draft was a URL: the page is not returned, so there is nothing to index. |
text | The passage as it appears in the draft (for a URL, in the page). null on a citation's position, whose row carries the statement. |
A claim row's positions is null only when the claim could not be placed
(and once a zero-retention draft has expired). Offsets count code points, not UTF-16 units: in
JavaScript, slice with Array.from(text).slice(start, end).join(""), since
text.slice(start, end) shifts after an emoji. POST /extract with
locate: true returns the same positions for the claims it lists.
Suggested edits
Set escalate.suggest_edits: true and every claim with a suggested_rewrite
also gets the smallest edits to your draft that make it say what the rewrite says: usually one
word or figure, in the draft's own language, with the rest of your wording left as it was. The
rewrite comes from the claim's deep check when it has one. A claim that stays on its quick
verdict gets one from the quick check when that check found it False or Mostly False with high
confidence, on assessment.suggested_rewrite; the issue's source says
which check it came from. A quick verdict is a first read, and so is its rewrite. Edits cost no
extra credits and arrive with the check that produced their rewrite. The review completes once
they are settled; until then status reads verifying, and
review.completed carries them.
"suggested_edits": {
"status": "completed",
"edits": [
{ "position": 0, "start": 23, "end": 26, "text": "20%", "replacement": "15%" }
]
}
| Field | Meaning |
|---|---|
status | pending while the edits are computed; completed once they are settled, which includes settling on none. |
edits | null while pending. [] when no edit could be made safely (the passage words the claim differently, the change would be more than a small edit, or it did not pass the checks), and also when computing it failed on our side. |
start, end, text | The span to replace, in the text you sent, in Unicode code points, end exclusive, as in positions. text is exactly that slice: compare it before you apply the edit, so a draft that changed since is never edited in the wrong place. |
position | Which of the row's positions the edit sits in, for grouping edits by passage. |
replacement | What replaces the span; an empty string deletes it. |
The block is null when you did not ask, when the claim got no completed deep check
with a rewrite (a True verdict has none), when the claim could not be placed in the draft, when
the passage is not in one of English, Spanish, German, French, Italian, Portuguese, Dutch,
Swedish, Danish, Norwegian, Finnish or Bulgarian, or once a zero-retention draft has expired.
Each issues[] row carries a copy of its claim's block.
An edit is built only from the deep check's suggested_rewrite and checked by other
models before it is returned, but it is not itself verified: review it before you publish it.
Edits sit on the claim row and never inside verification, because they quote your
draft and a verification can be shared. Two claims that share a passage can return edits that
overlap; apply one of them. To apply a row's edits, work from the end of the text back, so the
earlier offsets stay valid:
chars = list(text) # code points
for e in sorted(edits, key=lambda e: e["start"], reverse=True):
if "".join(chars[e["start"]:e["end"]]) == e["text"]:
chars[e["start"]:e["end"]] = list(e["replacement"])
edited = "".join(chars)
Check only the links #
Set escalate.max_assessments to 0 and
escalate.max_citations above it, and the review runs no quick check and no deep
check on the claims. It checks the citations and nothing else, and
credits.charged is what the checked citations cost.
curl -X POST https://lenz.io/api/v1/review \
-H "Authorization: Bearer $LENZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "...",
"escalate": { "max_assessments": 0, "max_citations": 20 }
}'
escalate.max_citations sits beside max_assessments and
max_verifications, from 0 to 20. A review that checks citations needs at least
1 credit on the account to be accepted, with or without quick checks. A draft with citations and no checkable claim still completes; with
neither, the review fails as no_claim, as today.
Check the rest in a second request #
Send the unchecked citations to POST /citecheck
as pairs, up to 20 per request: each item's sentence (or your own wording of what it is cited for)
with its doi when it has one (which keeps the DOI checks), otherwise its
cited_url. The pairs skip the pairing step, so no text is rebuilt. Send one
batch at a time: an account runs at most three checks at once. Send the unchecked claims to POST /assess as
claims.
import time
import requests
BASE = "https://lenz.io/api/v1"
H = {"Authorization": f"Bearer {LENZ_API_KEY}"}
review = requests.get(f"{BASE}/reviews/{review_id}", headers=H).json()
rest = review["more_citations"] or []
for i in range(0, len(rest), 20):
pairs = [
# a DOI keeps the DOI checks; otherwise the link
{"statement": c["sentence"], **({"doi": c["doi"]} if c["doi"] else {"url": c["cited_url"]})}
for c in rest[i:i + 20]
]
r = requests.post(f"{BASE}/citecheck", headers=H, json={"pairs": pairs})
r.raise_for_status() # a 429 citecheck_in_flight means three checks are already running
check_id = r.json()["citecheck_id"]
# one batch at a time: wait for this check before sending the next
while True:
check = requests.get(f"{BASE}/citechecks/{check_id}", headers=H).json()
if check["status"] in ("completed", "failed"):
break
time.sleep(check["poll_after_seconds"] or 10)
if review["more_claims"]:
requests.post(f"{BASE}/assess", headers=H, json={"claims": review["more_claims"]})
Each checked citation costs 1 credit, as inside a review, and each assessed claim 1.