# Verify a quote against text you hold

Last updated: 2026-10-13

`POST /v1/verify/text-quote` checks a quote against text the caller already has: a file it read, a tool result, a document. Nothing is fetched, so there are no robots, DNS or bot-wall failures. The answer is `exact`, `fuzzy` or `none`, with a score, offsets and context, under a signed receipt. Price: US$0.005 per call.

Use it when the source is already in hand. To check a quote against a live web page, use [Verify a quote](/docs/verify-quote), which fetches the page.

## Request

```json
{
  "text": "Refunds are paid within 14 days of purchase, to the card that was used.",
  "quote": "refunds are paid within 14 days",
  "match": "fuzzy",
  "case_sensitive": false
}
```

| Field | Required | Meaning |
|---|---|---|
| `text` | yes | the source text, as sent: a non-empty string of at most 50,000 characters (code points) and 64 KB of UTF-8 |
| `quote` | yes | the quote, at most 1,000 characters after normalisation |
| `start`, `end` | no | both or neither: a slice of `text` to check, in characters (code points), 0-based, `end` exclusive, at most 1,000 characters |
| `match` | no | `fuzzy` (default) also accepts small differences; `exact` needs the same text after normalisation; `passages` treats the quote as a paraphrased claim and returns the passages of the text that share its words (not with `start` and `end`) |
| `case_sensitive` | no | default `false`; must be `false` with `passages` |
| `threshold` | no | fuzzy similarity needed, 0.5 to 1, default 0.9 |

`url` is refused: this route never fetches. Every field is checked before payment, so a bad request is answered with 400 and not charged.

## Offsets

Two coordinate systems, named differently:

- `start` and `end` in the request are **raw** code-point offsets into `text` as sent. With them, only that slice is matched, so the answer is whether the text at those offsets says the quote. `result.declared` echoes them with `slice_sha256` (the hash of the raw slice) and `slice_matches`.
- `result.start` and `result.end` in a search (no offsets sent) are **normalised**: offsets into the text after normalising (curly quotes, spaces and case folding for matching, as on `verify/quote`). `normalised_sha256` is the hash of that normalised text.

## Response

```json
{
  "result": {
    "match": "fuzzy", "score": 0.972, "start": 0, "end": 34,
    "context": "Refunds are paid within 14 days of purchase",
    "occurrences": 1, "normalised_sha256": "…",
    "source": { "kind": "supplied", "text_sha256": "…", "code_points": 79 }
  },
  "injection_flags": [],
  "advice": "The supplied text contains a close match to this quote, with small differences. This says nothing about where the text came from.",
  "receipt": "eyJ…",
  "tier": "paid"
}
```

- `injection_flags` lists instruction-like text found in the supplied text. Agents paste untrusted output, so read them.
- The receipt is the caller's claim: it attests the hash of the text (`source.text_sha256`) and the match, not that the text came from any particular site.

## Errors

A 400 with `invalid_request` covers: an unknown field (including `url`), a missing or empty `text` or `quote`, `start` or `end` alone, a non-integer or negative offset, `start` not below `end` or `end` past the text (`reason` `offset_out_of_range`), a slice over 1,000 characters, text over 50,000 characters, a lone surrogate in `text` or `quote`, and offsets with `match` `passages`. A body over 64 KB gets 413. A fuzzy match too costly for the matcher gets 422 `match_too_costly`, not charged.
