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.
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"]) 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.001The 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}}'{
"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"
}
]
}