Skip to content

New: deep research, balance and auto-reload tools in the keyless MCP server. Install in one line

Convert any web page to clean Markdown in Python

Send the page URL to ContextFetch's /v1/contents endpoint with text set to true and you get the page back as clean Markdown, tables included. Add a highlights query to get only the passages about your topic. Each page that loads costs one credit, pages that fail cost nothing, and the code below needs nothing beyond the Python standard library.

Verified Python/v1/contents/r/{url}

1. Get an API key

Claim a free key with one POST request. It needs no signup and comes with 2,500 credits, and one free key is issued per network. Export it so the code below can read it.

You can also try the API with no key at all: requests without an Authorization header get 25 free calls per IP address per UTC day.

curl -X POST https://api.contextfetch.dev/v1/keys/free# {"apiKey": "cf_live_...", "credits": 2500}export CONTEXTFETCH_API_KEY="cf_live_..."

2. Fetch the page as Markdown

POST the URL to /v1/contents with text: true. The response has one entry in results per page that loaded, each with url, title and text, where text is the page as Markdown. You can send up to 10 URLs in one call, and each page that loads costs one credit.

A page that fails does not fail the whole call. It comes back in statuses with status: "error" and the reason in error.message, such as HTTP 404, and it is not billed. Check statuses before you assume every URL produced a result.

res = post("/v1/contents", {    "urls": ["https://docs.python.org/3/library/asyncio-task.html"],    "text": True,})page = res["results"][0]print(page["title"], len(page["text"]))

3. Keep only the passages you need

Whole pages are long. If you only need the part that answers a question, add a highlights object with a query. Each highlight is a 400 to 700 character Markdown chunk chosen by a cross-encoder for that query, and highlightScores gives each chunk's score from 0 to 1. highlightsPerUrl sets how many chunks you get per page.

This is the cheapest way to feed a page to an LLM: you pass the two or three chunks that matter instead of the whole document, and the model has less unrelated text to get distracted by.

4. Or use the reader URL

For quick jobs there is a GET endpoint: put any public URL after /r/ and you get text/markdown back. Add ?q= with a question to prune the page to the relevant chunks. The X-Token-Savings response header tells you what percentage of tokens the Markdown or the pruning saved.

The reader also rewrites GitHub blob URLs to raw files and arXiv PDF links to their HTML versions, so you get text you can actually use. Because it handles one URL, a page that fails to load comes back as HTTP 502 fetch_failed; see the error guide.

curl -s "https://api.contextfetch.dev/r/https://docs.python.org/3/library/asyncio-task.html?q=task+groups" \  -H "Authorization: Bearer $CONTEXTFETCH_API_KEY"

The complete code

Python, runnable as is with CONTEXTFETCH_API_KEY set. We ran it against the live API; the output below is what it printed.

import jsonimport osimport urllib.request API = "https://api.contextfetch.dev"KEY = os.environ["CONTEXTFETCH_API_KEY"]  def post(path, body):    req = urllib.request.Request(        API + path,        data=json.dumps(body).encode(),        headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"},    )    with urllib.request.urlopen(req, timeout=90) as resp:        return json.load(resp)  url = "https://docs.python.org/3/library/asyncio-task.html"res = post("/v1/contents", {    "urls": [url],    "text": True,    "highlights": {"query": "task groups", "highlightsPerUrl": 2},}) for status in res["statuses"]:    if status["status"] != "success":        print("failed:", status["id"], status["error"]["message"]) for page in res["results"]:    print(page["title"])    print(f"{len(page['text']):,} characters of Markdown")    print(page["text"][:300])    print("\nBest passage for 'task groups':")    print(page["highlights"][0][:400]) print("\ncost: $", res["costDollars"]["total"]) 
OutputRecorded
Coroutines and tasks
44,749 characters of Markdown
# Coroutines and tasks

This section outlines high-level asyncio APIs to work with coroutines and Tasks.

**Source code:** Lib/asyncio/coroutines.py

Coroutines declared with the async/await syntax is the preferred way of writing asyncio applications. For example, the following snippet of code print

Best passage for 'task groups':
- *class* asyncio.TaskGroup
- An asynchronous context manager
holding a group of tasks.
Tasks can be added to the group using `create_task()`.
All tasks are awaited when the context manager exits.
Added in version 3.11. 
  - 
create\_task(*coro*, *\**, *name=None*, *context=None*, *eager\_start=None*, *\*\*kwargs*)
  - Create a task in this task group.
The signature matches that of `asyncio.create

cost: $ 0.001

The same call with curl

The raw HTTP request, and the response it returned (long text fields and lists shortened).

curl -s https://api.contextfetch.dev/v1/contents \  -H "Authorization: Bearer $CONTEXTFETCH_API_KEY" \  -H "Content-Type: application/json" \  -d '{"urls": ["https://docs.python.org/3/library/asyncio-task.html"], "text": true, "highlights": {"query": "task groups", "highlightsPerUrl": 2}}'
ResponseRecorded
{
  "requestId": "7310406b354e49bd9210525fa13d0e38",
  "results": [
    {
      "id": "a23ba312-a663-5484-83cd-f32f78d8d89b",
      "url": "https://docs.python.org/3/library/asyncio-task.html",
      "title": "Coroutines and tasks",
      "score": null,
      "publishedDate": "2026-10-17",
      "text": "# Coroutines and tasks\n\nThis section outlines high-level asyncio APIs to work with coroutines and Tasks.\n\n**Source code:** Lib/asyncio/coroutines.py\n\nCoroutines declared with the async/await syntax is the preferred way of writing asyncio applications. For example, the following s…",
      "highlights": [
        "- *class* asyncio.TaskGroup\n- An asynchronous context manager\nholding a group of tasks.\nTasks can be added to the group using `create_task()`.\nAll tasks are awaited when the context manager exits.\nAdded in version 3.11. \n  - \ncreate\\_task(*coro*, *\\**, *name=None*, *context=None*…",
        "In the case where a task group is cancelled externally and also must\nraise an `ExceptionGroup`, it will call the parent task’s\n`cancel()` method. This ensures that a\n`asyncio.CancelledError` will be raised at the next\n`await`, so the cancellation is not lost.\n\nTask groups preserv…"
      ],
      "highlightScores": [
        0.9980562155652424,
        0.9924491684892667
      ],
      "prunedContext": "Task groups combine a task creation API with a convenient and reliable way to wait for all tasks in the group to finish.\n\n- *class* asyncio.TaskGroup\n- An asynchronous context manager\nholding a group of tasks.\nTasks can be added to the group using `create_task()`.\nAll tasks are a…",
      "textSource": "page"
    }
  ],
  "costDollars": {
    "total": 0.001
  },
  "searchTimeMs": 269,
  "searchTime": 269,
  "statuses": [
    {
      "id": "https://docs.python.org/3/library/asyncio-task.html",
      "status": "success"
    }
  ]
}

Questions

How much does it cost?

One credit per URL that loads. Credits cost $1 per 1,000 at the base pack rate, and URLs that fail are not billed. A free key includes 2,500 credits.

Which URLs can it fetch?

Public http and https URLs. Private, loopback and link-local addresses are rejected with HTTP 400 url_not_allowed, also after redirects. A fetch follows at most 5 redirects, reads at most 5 MB and times out after 15 seconds.

What happens if a page fails to load?

In /v1/contents the call still succeeds: the failed URL appears in statuses with status error and a reason such as HTTP 404, and it costs nothing. The /r/ reader, which handles a single URL, answers HTTP 502 fetch_failed instead.

Can I get only part of a page?

Yes. Pass highlights with a query to get the most relevant 400 to 700 character chunks with their scores, or use the reader with ?q= to prune the page down to the chunks that match your question.

Run it with your own key

A free key has 2,500 searches and needs no signup. Agents can claim one themselves with the POST below.

curl -X POST https://api.contextfetch.dev/v1/keys/free
Get a free key