# Verify a quote

Last updated: 2026-10-10

`POST /v1/verify/quote` fetches a page and reports whether a quote appears on it: `exact`, `fuzzy` or `none`, with offsets, surrounding context, the page verdict and a signed receipt. For a paraphrased claim, `"match": "passages"` returns instead the passages that share most of its words (below). Price: US$0.01 per call.

It checks your exact words, not a reworded version, against the page and returns offsets and page hashes in a signed receipt, all in one US$0.01 call.

Use it before citing a page, to check that the words you attribute to it are on it, and that the page you got is real content rather than a bot wall or an error page.

For 9 or more quotes on one page, [verify/quotes](/docs/verify-quotes) costs less: one call checks up to 20.

## Request

```json
{
  "url": "https://www.iana.org/help/example-domains",
  "quote": "These domains may be used as illustrative examples in documents",
  "match": "fuzzy",
  "case_sensitive": false,
  "threshold": 0.9
}
```

| Field | Required | Meaning |
|---|---|---|
| `url` | yes | http or https URL, at most 2,048 characters |
| `quote` | yes | text to look for, or with `passages` the claim to find support for; at most 1,000 characters after normalisation |
| `match` | no | `fuzzy` (default) also accepts small differences; `exact` needs the same text after normalisation; `passages` finds the passages that best share a paraphrased claim's words |
| `case_sensitive` | no | default `false`; must be `false` with `passages` |
| `threshold` | no | fuzzy similarity needed, 0.5 to 1, default 0.9 |

Before matching, both the page text and the quote are normalised: Unicode NFKC, curly quotes and dashes become plain ones, invisible characters are dropped, and all whitespace becomes one space.

## Response

From the free sample (`GET /v1/sample/quote`), shortened:

```json
{
  "result": {
    "match": "fuzzy",
    "score": 0.963,
    "start": 211,
    "end": 238,
    "context": "...the agency said that emissions fell 12 % in 2025, after two years of slow growth...",
    "occurrences": 1,
    "normalised_sha256": "086a29af…"
  },
  "page": {
    "requested_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page",
    "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page",
    "http_status": 200,
    "content_kind": "real",
    "signals": ["status_200"],
    "title": "Regional climate report 2025 (demo page)",
    "content_sha256": "0e429607…",
    "retrieved_at": "2026-10-09T14:19:34.903Z"
  },
  "injection_flags": [],
  "advice": "The page loaded and looks like real content.",
  "receipt": "eyJhbGciOiJFZERTQSIs…",
  "tier": "paid",
  "rail": "x402",
  "network": "eip155:84532"
}
```

- `match` is `exact`, `fuzzy` or `none`. `score` is the similarity from 0 to 1.
- `start` and `end` are code-point offsets into the normalised page text. `normalised_sha256` is the SHA-256 of that text, so anyone holding the page text can check the offsets.
- `occurrences` counts exact matches.
- Read `page.content_kind` before relying on `match`. A `none` on a `bot_wall` or `js_shell` page means we could not read the page, not that the quote is absent from it. See [How it works](/docs/how-it-works).

## Passages: a paraphrased claim

An agent rarely quotes word for word. With `"match": "passages"`, `quote` is a claim (for example "The agency reported a 12 percent drop in emissions during 2025") and the answer is the 3 passages of the page that share most of its words:

On the demo page ([`https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page)), shortened; try it free with [`GET /v1/sample/quote?match=passages`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote?match=passages):

```json
{
  "result": {
    "match": "passages",
    "score": 0.583,
    "start": 115,
    "end": 271,
    "occurrences": 3,
    "passages": [
      { "start": 115, "end": 271, "score": 0.583, "text": "It is not a real report and its numbers are made up. In its annual summary the agency said that emissions fell 12 % in 2025, after two years of slow growth.", "missing": ["drop"], "flags": ["negation_mismatch"] },
      { "start": 357, "end": 513, "score": 0.217, "text": "The agency also noted that the share of renewable power rose to 41 percent, and that the next report will cover water use and air quality across the region.", "missing": ["12", "drop", "emissions", "2025"], "flags": ["number_mismatch"] },
      { "start": 0, "end": 114, "score": 0.136, "text": "Regional climate report 2025 This is a demo page served by AttestPage so that agents can try every route for free.", "missing": ["agency", "12", "percent", "drop", "emissions"], "flags": [] }
    ],
    "scorer": "lexical-v2",
    "note": "Passages ranked by how many of the claim's words (weighted by rarity on the page) and word pairs they share. This is evidence for you to judge, not a verdict: ..."
  }
}
```

Here "reported" counted as found because the passage has "report". The best passage does support the claim, but only a reader can tell that "fell 12 %" means a 12 percent drop. It is flagged `negation_mismatch` only because its first sentence says "not a real report": a flag tells you where to look, not that the passage contradicts the claim.

- **This is evidence, not a verdict.** A score measures shared words, not meaning. "Emissions did not fall" shares as many words as "emissions fell". Read each passage, and check negation, numbers, dates and the words in `missing`.
- **Passages.** A passage is a sentence, or two neighbouring ones; text with no sentence ends is cut every 40 words. A sentence ends at `.`, `!` or `?` before a space, or at `。`, `！` or `？`, but not after a short abbreviation such as "e.g.", "i.e.", "Dr.", "Mr." or an initial ("J. Smith"); "etc." ends one only before a capital. Passages do not overlap and are sorted by `score`, highest first. The top-level `score`, `start`, `end` and `context` are those of the best passage, and `occurrences` counts the passages.
- **Score.** From 0 to 1: 0.75 × the share of the claim's words the passage holds, each weighted by how rare it is on the page, plus 0.25 × the share of the claim's neighbouring word pairs it holds.
- **How words are compared.** Without case, with light English stemming ("emissions" meets "emission"), and ignoring common English words such as "the" and "was". `%` counts as "percent". In Chinese and Japanese, each character counts as a word.
- **Offsets and text.** `start` and `end` are code-point offsets into the normalised page text, as above. `text` is cut to 800 characters with `...` when the passage is longer.
- **missing.** Lists the claim's words that the passage lacks, at most 20.
- **flags.** Marks a passage worth a closer read; it never changes the score or the order. `negation_mismatch`: the claim or the passage has an English negation ("not", "no", "never", "n't", "without" and similar) and the other has none. `number_mismatch`: the claim has a number the passage lacks and the passage has one the claim lacks, such as 12 against 15 (`12 %`, `12 percent` and `12.0` count as the same number, and so do `1,000` and `1000`). An empty list does not mean the passage agrees with the claim.
- **No match.** `match` is `none` with an empty `passages` list when no passage shares a word.
- **Errors.** A claim made only of common words or punctuation gets 400.
- **Same answer every time.** No language model is used, so the same page and claim give the same answer. `scorer` names the method (now `lexical-v2`) and changes whenever the same page and claim could give different passages, scores or flags.
- **Cost.** The cost is linear in the page size, so passages never gets `match_too_costly`.
- **Receipt.** The receipt signs `mode`, `scorer` and each passage's `start`, `end` and `score`. The text can be recovered from the page at those offsets, and `missing` and `flags` from that text, the claim and the `scorer`.
- **Price.** The same as any other `verify/quote` call.

## PDF

A PDF page is matched like any other page. Its text layer is read as described in [Fetch: PDF](/docs/fetch), with no OCR.

- A match on a PDF also has `pdf_page` and `pdf_page_end`: the 1-based pages where it starts and ends. Each passage has them too.
- `start` and `end` are still offsets into the whole normalised text. In that text, each page break is one space.
- Only text inside each page's box is read, as a reader sees it. A quote from text drawn outside the page is not found.
- The receipt signs `pdf_page` and `pdf_page_end` next to the offsets.

Only the full service reads PDFs: an edge server answers 501 `not_on_edge` (`details.reason` `pdf`) for one, not charged. Limits are in [Limits](/docs/limits).

## Charging

The quote and options are checked before payment: a bad request gets 400 and is not charged. A quote too costly to match fuzzily gets 422 `match_too_costly`, not charged; use `"match": "exact"` or a shorter quote. A URL whose host name does not resolve gets 422 `host_not_found`, not charged. Results about the page itself, including 404s, bot walls and timeouts, are charged. See [Payments](/docs/payments).
