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 itemMeaning
statementRequired. What the source is cited for, up to 1,000 characters.
url or doiExactly one. A public http(s) address, or a whole DOI such as 10.1038/nature12373.
quotesOptional, 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_journalOptional, 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.

Whenstatusoutcome
an issue found, nothing failedcompletedissues_found
at least one citation checked, no issue, nothing failedcompletedclean
every citation uncheckedcompletedunchecked
a check failed on our side, or ran out of timecompletedincomplete
a text with no citation (no_citations), or the pairing step unavailable (upstream_unavailable)failedunchecked

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

HTTPcodeWhenWhat to do
422validation_errorneither or both of text and pairs, max_citations with pairs, or a field out of rangeThe error names the field and its limit.
422url_inputtext is one URLSend the page's text, with its links, to check the sources it cites.
422invalid_doia doi that is not a whole DOISend the DOI alone, like 10.1038/nature12373.
422quote_not_in_statementa quote its statement does not containEach quote must be words of its statement.
422webhook_secret_missinga webhook_url on a credential with no signing secretCreate one on the API credentials page, or read GET /me/webhook-secret for an OAuth connection.
422idempotency_body_mismatchan Idempotency-Key reused with a different bodyUse a new key for a new request.
402no_creditsunder 1 credit at acceptanceTop up, or wait for the monthly allowance.
409idempotency_conflicta check with this Idempotency-Key is still being createdRetry shortly; citecheck_id names that check once it exists.
429citecheck_in_flightthree checks already running on the accountWait for one to finish; honour Retry-After.
503capacitythe services that read or judge pages are saturatedRetry after Retry-After. Nothing was charged.
503citations_unavailablecitation checking is switched off right nowRetry 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 what reference returns.
  • 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 as https://.
  • DOIs, as doi:10.1038/nature12373 or 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 as unchecked (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 text that is one URL. The page is read for its claims, but a page's text arrives without its links, so summary.citations_skipped reads url_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.

findingWhat it meansWhat to doIssue
doi_not_foundThe 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_foundEvery provider that reached the link got "not found" (404 or 410).Ask for the working address, or an archived copy of the page.yes
contradictedThe 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_sourceWords 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_sourceThe 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_mismatchFor 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_supportedThe 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
supportedThe source says it. snippet is the passage.Nothing.no
uncheckedThe 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_reasonMeaning
no_textThe source gave no text that could be read.
partial_textOnly part of the source could be read, so what it leaves out cannot be judged.
login_requiredThe site shows its pages only after a login (Facebook, Instagram, Threads, LinkedIn).
unsupported_siteLinks of this kind are not read. Video links are among them.
no_statementNo 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_pairingLenz 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_urlThe link is not a public http(s) address. It is not sent to a provider.
other_versionFor 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.
inconclusiveThe source was read and does not settle the sentence either way.
ambiguous_referenceThe 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 checkMeaning
statusProgress: pending, running, completed, failed. result is null until the check has ended.
page_readWhat reading the source gave: full, partial (cut short, a teaser or an abstract alone), not_found, none.
source_urlThe address the text was read from: the final address after redirects, or the free copy of a paper.
source_versionFor a DOI, the version read: published, accepted or submitted. null for a plain link.
supportThe support check: supported, partly_supported, contradicted, not_in_source, unchecked.
snippetThe passage from the source the support answer rests on, checked against the page text.
rationaleA reviewer's one-sentence reasoning. Not a checked source.
quoteThe 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_quoteThe 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, registeredThe 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, hintOn 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
}
FieldMeaning
citations_foundEvery 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_selectedThe ones this review checks: the first ones, in the draft's order.
citation_limit / citation_limit_reachedThe resolved cap, and whether the draft has more citations than it.
citation_checksThree 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_issuesThe number of rows in citation_issues[].
citations_skippedWhy 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.

FieldMeaning
more_citationsThe citations found beyond the ones checked, up to 100, in the draft's order: index, reference, cited_url, doi, sentence, position.
more_claimsThe claims found beyond max_assessments, as plain strings, up to 20 in all.
more_claim_locationsWhere 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:

FieldMeaning
start, endWhere 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.
textThe 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%" }
  ]
}
FieldMeaning
statuspending while the edits are computed; completed once they are settled, which includes settling on none.
editsnull 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, textThe 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.
positionWhich of the row's positions the edit sits in, for grouping edits by passage.
replacementWhat 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.