# Verify quotes on one page

Last updated: 2026-10-10

`POST /v1/verify/quotes` checks 1 to 20 quotes against one web page in one call. The page is fetched once, and each quote gets the same answer as [Verify a quote](/docs/verify-quote) (`exact`, `fuzzy` or `none`, score, offsets, context), all under one signed receipt. Price: US$0.08 per call, whether it carries 1 quote or 20, so from 9 quotes up it costs less than one `verify/quote` call per quote.

Use it when an answer quotes the same source several times: every quote is checked against the same copy of the page in one paid call.

## Request

```json
{
  "url": "https://www.iana.org/help/example-domains",
  "quotes": [
    "These domains may be used as illustrative examples in documents",
    "without prior coordination with us"
  ],
  "match": "fuzzy"
}
```

| Field | Required | Meaning |
|---|---|---|
| `url` | yes | the page, http or https, at most 2,048 characters |
| `quotes` | yes | 1 to 20 strings, each at most 1,000 characters after normalisation |
| `match` | no | for every quote: `fuzzy` (default) also accepts small differences; `exact` needs the same text after normalisation; `passages` treats each quote as a paraphrased claim (below) |
| `case_sensitive` | no | default `false`; must be `false` with `passages` |
| `threshold` | no | fuzzy similarity needed, 0.5 to 1, default 0.9 |

The URL and every quote are checked before payment. If the URL is malformed, blocked or disallowed by robots.txt, or any quote is empty, too long or not a string (or, with `passages`, has no word to compare), the whole request is refused with 400 or 403 and not charged. The error's `details.field` names the quote, for example `quotes[2]`. Other fields are refused too.

## Response

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

```json
{
  "results": [
    {
      "match": "exact",
      "score": 1,
      "start": 384,
      "end": 431,
      "context": "... The agency also noted that the share of renewable power rose to 41 percent, and that the next report will cover...",
      "occurrences": 1,
      "normalised_sha256": "086a29af…"
    },
    {
      "match": "fuzzy",
      "score": 0.963,
      "start": 211,
      "end": 238,
      "context": "...In its annual summary the agency said that emissions fell 12 % in 2025, after two years of slow growth...",
      "occurrences": 1,
      "normalised_sha256": "086a29af…"
    },
    {
      "match": "none",
      "score": null,
      "start": null,
      "end": null,
      "context": null,
      "occurrences": 0,
      "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",
    "title": "Regional climate report 2025 (demo page)",
    "content_sha256": "0e429607…"
  },
  "injection_flags": [],
  "advice": "The page loaded and looks like real content.",
  "summary": { "exact": 1, "fuzzy": 1, "none": 1, "failed": 0 },
  "receipt": "eyJhbGciOiJFZERTQSIs…",
  "tier": "paid"
}
```

- `results` has one entry per quote, in request order. Each is the `result` of [Verify a quote](/docs/verify-quote): `match`, `score`, `start`, `end`, `context`, `occurrences` and `normalised_sha256` mean the same, and give the same values a `verify/quote` call on that page would.
- `page` is the page verdict, hashes and network evidence, once for all the quotes.
- Read `content_kind` before relying on `match`. A `none` on a page that is not `real` (a 404, bot wall or JavaScript shell) means we could not read the page, not that the quote is absent from it.
- `summary` counts the verdicts; `failed` counts quotes that carry an `error` instead of a verdict (below).
- The receipt signs the page's URL, status, `content_kind`, hashes and evidence, and per quote the SHA-256 of the quote and the verdict. It does not contain the quote text. Check it with [`POST /v1/receipt/verify`](/docs/receipt-verify).

## Passages: paraphrased claims

With `"match": "passages"`, each quote is a claim. Each result is then the one described in [Verify a quote: passages](/docs/verify-quote): up to 3 passages from the page that share most of the claim's words, each with `start`, `end`, `score`, `text`, `missing` and `flags`, plus `scorer` and `note`. These are evidence for you to judge, not a verdict.

- `match` is `passages` or `none`, and `summary` adds a `passages` count.
- The receipt signs each claim's passages (`start`, `end`, `score`) and the `scorer`.
- The work is linear in page size, so passages never fail with `match_too_costly`.

The price is the same.

## PDF

A PDF page is read and matched as in [Verify a quote: PDF](/docs/verify-quote). Each match and passage on it has `pdf_page` and `pdf_page_end`, and the receipt signs them. If the PDF cannot be queued for reading, the call gets 503 `over_capacity` (`details.reason` `pdf_queue_full`) and is not charged.

## Partial failures

A quote that would cost too much to match fuzzily gets `"error": { "code": "match_too_costly", ... }` and null match fields; the other quotes are still checked and the call is charged. The fuzzy searches of one call also share a work limit (twice what one `verify/quote` call may use), spent in request order: a quote reached after it is used up gets the same error with a message saying so. If every quote fails this way, the call gets 422 `match_too_costly`, not charged, and `details.failed` lists only which quotes failed. Use `"match": "exact"` or shorter quotes.

This route shares the per-address count of uncharged failed batches and the match queue with [Verify citations](/docs/verify-citations): over the count it answers 429 `rate_limited` with `details.reason` `uncharged_failures`, and when the queue is full 503 `over_capacity` with `details.reason` `match_queue_full`, both with a `Retry-After` header and not charged. See [Limits](/docs/limits).

## Edge servers

Only the full service runs this route: an edge server answers 501 `not_on_edge`, not charged.

## Charging

A URL whose host name does not resolve gets 422 `host_not_found`, not charged. If the page turns out to be disallowed by robots.txt or rate limited after payment (for example after a redirect), the call answers with an error and is not charged. Results about the page itself, including 404s, bot walls and timeouts, are charged. See [Payments](/docs/payments).

A free-trial call covers at most 3 quotes. A larger one is not run as a trial: it gets the normal 402 offer, with `details.reason` `trial_too_large`, and is not charged.
