# Fetch a page

Last updated: 2026-10-12

`POST /v1/fetch` fetches one URL (or up to 10 with `urls`) and tells you what came back: a `page.content_kind` verdict, HTTP status, final URL after redirects, title, hashes, injection flags and, if you ask, up to 20,000 characters of clean text. Price: US$0.002 per URL: one `url` is US$0.002, and `urls` costs US$0.002 times the number of URLs.

Use it when you need to know whether a URL gave you real content, a bot challenge, a JavaScript shell, a paywall or an error, with a receipt that records it. It is also how you notarize a web page: the signed receipt records what the page said and when.

## Request

```json
{ "url": "https://www.iana.org/help/example-domains", "return_text": true }
```

| Field | Required | Meaning |
|---|---|---|
| `url` | `url` or `urls` | http or https URL, at most 2,048 characters |
| `urls` | `url` or `urls` | instead of `url`: a list of 1 to 10 distinct URLs, each priced as one fetch; see [Several pages in one call](#several-pages-in-one-call) |
| `return_text` | no | also return up to 20,000 characters of clean text (per page); default `false` |

## Response

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

```json
{
  "page": {
    "requested_url": "https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/sample/page",
    "final_url": "https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/sample/page",
    "http_status": 200,
    "content_type": "text/html",
    "content_kind": "real",
    "signals": ["status_200"],
    "title": "Regional climate report 2025 (demo page)",
    "redirects": [],
    "content_sha256": "0e429607…",
    "raw_sha256": "179b9ac4…",
    "retrieved_at": "2026-10-09T14:19:34.909Z",
    "bytes": 847,
    "truncated": false
  },
  "text": "Regional climate report 2025\n\nThis is a demo page…",
  "text_chars": 636,
  "text_truncated": false,
  "injection_flags": [],
  "advice": "The page loaded and looks like real content.",
  "receipt": "eyJhbGciOiJFZERTQSIs…",
  "tier": "paid",
  "rail": "x402",
  "network": "eip155:8453"
}
```

- Receipts are signed by `iss` https://still-rapids-9yt7.here.now (keys at /.well-known/jwks.json), not by the origin you called. Check one free with `POST /v1/receipt/verify` ([docs](/docs/receipt-verify)).
- `page.content_kind` and `page.signals` are explained in [How it works](/docs/how-it-works). `advice` is one plain sentence about the verdict.
- `content_sha256` hashes the full extracted text, not the 20,000-character copy. `raw_sha256` hashes the bytes as received. `content_sha256` is null when no body was read.
- `injection_flags` lists prompt-injection patterns found in the text, such as `instruction_override` or `hidden_instructions`. They are flags, not a verdict: an empty list means none of our patterns matched.
- `text` is null unless you set `return_text`.

## Several pages in one call

Send `urls` instead of `url` to fetch up to 10 pages in one paid call:

```json
{ "urls": ["https://www.iana.org/help/example-domains", "https://www.iana.org/domains/reserved"], "return_text": true }
```

- The price is US$0.002 times the number of URLs, on every rail: the 402 offer (x402 `PAYMENT-REQUIRED`, the L402 invoice) is made for that body, so send the same body when you pay. A credit key is charged the sats price times the count.
- The answer is `{ "results": [...], "tier", "rail", "network" }`. `results` has one entry per URL, in the order sent: `{ "url", "page", "text", "text_chars", "text_truncated", "injection_flags", "advice", "receipt" }`, each as for one `url`.
- Each page has its own receipt, the same size as a one-page receipt, so [Recheck](/docs/recheck) and `POST /v1/receipt/verify` take each one alone. Each also signs `result.batch: { "index", "count" }` (its place in the call) and the `request_sha256` of the whole request.
- Each URL may appear once; a repeat is refused with 400 before payment. Send `url` or `urls`, not both.
- As for one page, a bad URL, a blocked address or a host name that does not resolve is refused before payment (with `details.field` such as `urls[2]`). If any page turns out to be disallowed by robots.txt or rate limited after payment, the whole call answers with that error and is not charged.
- A free trial call covers at most 5 URLs; a larger one gets the normal 402 offer, `details.reason` `trial_too_large`.

## PDF

An `application/pdf` page is read too, within the same 2 MB and 10-second limits. Only its text layer is read, with no OCR, so a scanned PDF comes back as `empty` with the signal `pdf_no_text_layer`.

- `signals` includes `pdf`. `title` is the PDF's own title, if it has one.
- `text` has the pages in order, separated by a form feed (`\f`).
- Only text inside each page's box is read, as a reader sees it. Text drawn outside the page is not in `text`.
- `page.pdf` gives `page_count`, `pages_read` and `truncated`. Very long PDFs stop at a page or character cap, with the signal `pdf_truncated`. The receipt signs these as well.
- A PDF we could not read is `unsupported_type` with a signal saying why: `pdf_encrypted`, `pdf_invalid`, `pdf_over_budget`, `pdf_timeout` or `pdf_out_of_memory`. It is charged like any other page we cannot read.
- When too many PDFs are waiting to be read, the call gets 503 `over_capacity` (`details.reason` `pdf_queue_full`) with `Retry-After`. It is not charged.

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).

## Later: has it changed?

Keep the receipt. [Recheck](/docs/recheck) (`POST /v1/recheck`, US$0.003) takes it later, fetches the page again and says `unchanged`, `changed`, `gone` or `unreadable`, with a signed receipt naming both hashes.

## Charging

A bad URL, a blocked address or a host name that does not resolve is refused before payment and not charged. Results about the page itself, including 404s and bot walls, are charged: that is the answer you paid for. A fetch that read no page (a timeout, a failed connection, an undecodable body, too many redirects, or a redirect we refuse) answers 502 `upstream_unreachable` with `details.reason`, and is not charged. In a batch the call is charged once, when at least one URL read a page; if none did, it is 502 and not charged. See [Payments](/docs/payments).
