1. readfetch
  2. examples

Recipes

Eight jobs, one request each

Each block is a complete call against https://readfetch.com. The payment step — 402, sign accepts[0], resend with PAYMENT-SIGNATURE — is the same for all of them and is written out once in /docs. No SDK, no key: curl and jq are enough to run every one of these.

Ingest one article into a RAG store

/v1/text

You have a URL and want the body as Markdown, with the metadata a vector store wants to filter on.

shell
curl -s https://readfetch.com/v1/text \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/blog/post","format":"markdown"}' \
  | jq '{id: .url, title, text, author: .metadata.author, date: .metadata.date, bytes: .size}'
javascript
const res = await fetch("https://readfetch.com/v1/text", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ url: "https://example.com/blog/post", format: "markdown" }),
});
if (res.status === 402) {
  // quote is in the body's accepts[0] and in the PAYMENT-REQUIRED header:
  // sign it, then resend this exact request with PAYMENT-SIGNATURE.
  throw new Error("unpaid");
}
const page = await res.json();
if (page.billed === false) console.warn(page.hint); // nothing was charged
console.log({ url: page.url, title: page.title, text: page.text });

Research loop: search, then read the top results

/v1/search → /v1/text

You have a question and no URLs. One search, then a text call per result you decide to read.

shell
curl -s https://readfetch.com/v1/search \
  -H 'content-type: application/json' \
  -d '{"query":"rust async runtime","limit":5}' \
  | jq -r '.results[].url' \
  | head -3 \
  | while read -r u; do
      curl -s https://readfetch.com/v1/text \
        -H 'content-type: application/json' \
        -d "$(jq -nc --arg u "$u" '{url:$u}')" \
        | jq -c '{url, title, chars: (.text | length)}'
    done

The search is one payment; each text call is another. Read three of five results, not all of them — that is the whole cost argument for doing search and reading as separate calls.

A page that only fills in after JavaScript

/v1/scrape

The HTML is a shell: the results arrive after a fetch, so text extraction gets nothing. Render it once in Chromium and wait for the selector.

shell
curl -s https://readfetch.com/v1/scrape \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://example.com/app",
    "mode": "render",
    "format": "markdown",
    "advanced": {
      "wait_for": "#results",
      "scan_full_page": true,
      "include_links": true
    }
  }' \
  | jq '{title, markdown, links: (.links | length), billed}'

Use /v1/text instead whenever the content is already in the HTML — it is cheaper and it does not hold a browser slot. used_browser on a text response tells you whether a render rescued it.

Read a list of URLs for one payment

/v1/batch

A list of pages, one quote, results in the order you sent them.

shell
curl -s https://readfetch.com/v1/batch \
  -H 'content-type: application/json' \
  -d '{
    "urls": [
      "https://example.com/a",
      "https://example.com/b",
      "https://example.com/c"
    ],
    "pipeline": "text",
    "format": "markdown"
  }' \
  | jq '{delivered, failed, credit, pages: [.results[] | {url, billed, title}]}'

Priced per delivered page with a minimum, and the 402 quotes the whole list before anything runs. A page that does not deliver is not billed and shows up as credit: check billed per result, not just the top-level status.

Know when a page changes

/v1/diff

No storage on our side: you keep the hash, we tell you whether it moved and what changed.

first read
curl -s https://readfetch.com/v1/diff \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/pricing"}' \
  | jq -r '.hash' > pricing.hash
next run
curl -s https://readfetch.com/v1/diff \
  -H 'content-type: application/json' \
  -d "$(jq -nc --rawfile h pricing.hash --arg u 'https://example.com/pricing' '{url:$u, previous_hash:($h|rtrimstr("\n"))}')" \
  | jq '{changed, added, removed, diff}'

A no-change answer is billable: it is a fresh read of the page, not a cache hit. Send previous_content as well if you want a line diff rather than a hash comparison.

Text out of a PDF

/v1/pdf

A report, a paper or an invoice — page text plus one Markdown body, priced per document.

shell
curl -s https://readfetch.com/v1/pdf \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/report.pdf","max_pages":20}' \
  | jq '{page_count, title, markdown, billed, hint}'

Priced per document, so raising max_pages does not raise the price — it only bounds the work. Only %PDF- bodies are accepted; a PDF served as HTML comes back unbilled with a hint.

One section instead of a whole page

/v1/section

You need the "Authentication" part of a long API page, not forty thousand tokens of it.

shell
curl -s https://readfetch.com/v1/section \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://docs.example.com/api",
    "heading": "Authentication",
    "max_chars": 4000
  }' \
  | jq '{matched, chars, text}'

A heading that is not found is a 200 with an empty body and available_headings — pick one of those names and call again. selector works the same way for a CSS selector, up to twenty matches.

A whole site, a few pages deep

/v1/crawl → /v1/crawl/{id}

You want /blog, twenty pages, two levels. The cap is quoted before the job starts.

start the job
curl -s https://readfetch.com/v1/crawl \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://example.com",
    "max_pages": 20,
    "max_depth": 2,
    "include": ["/blog/*"]
  }'
# {"job_id":"job_ab12","status":"queued","pages_quoted":20,"hint":"GET /v1/crawl/{id} for progress"}
follow it
curl -s https://readfetch.com/v1/crawl/job_ab12

Billing is per delivered page, bounded by the quoted cap. If you do not know the shape of the site yet, /v1/map returns its sitemaps, feeds and robots rules for the price of one call.

When a recipe returns billed: false

Nothing was charged, and the response carries a hint naming the next call: a wall means /v1/scrape, empty text means the page needs a render, a binary body means /v1/fetch. /SKILL.md has the full symptom-to-next-call table, and /choose has the decision if you would rather pick the endpoint before writing the loop.