By Sean Donahoe · Published July 16, 2026 · Prices and limits are accurate as of this date.
How do I fetch a web page as clean markdown through FluxRouter?
Send the URL to POST /v1/fetch, get back clean markdown. Same key and same base URL (https://api.fluxrouter.ai) you already use for chat. The body is one field, url. The response is { markdown, format, url }, ready to hand straight to a model. Add render: true for JavaScript-heavy pages. Runs on any paid plan.
No HTML parser. No headless browser to stand up. No scraper to babysit. One HTTP call and the page comes back as markdown.
The rest of this page is the how-to: the call, what comes back, when to flip render, what it costs, and the errors your client should catch.
The easy button
Don't feel like reading a guide? Hand this to your coding agent and let it write the call:
Fetch this page as markdown through FluxRouter. POST to
https://api.fluxrouter.ai/v1/fetch with my Flux key (paid plan) and body
{"url": "<the page>"}. Use the "markdown" field from the JSON response.
That's the whole instruction. It's a single HTTP call, nothing to configure. One request goes out, markdown comes back.
From the Flux CLI it's a single line:
fluxrouter fetch https://example.com
Add --render on that same command when the page needs JavaScript to paint.
One gate before you wire it in: web fetch is a paid capability. A key without a paid plan comes back with 402, and the message says it straight: the key isn't eligible yet, use one on a paid plan. So catch the 402 and switch to a paid-plan key, don't read it as a bug in your call.
Make the call
The request is about as small as a request gets. POST /v1/fetch, Content-Type: application/json, a Bearer key, and a body with a url in it.
curl:
curl https://api.fluxrouter.ai/v1/fetch \
-H "Authorization: Bearer $FLUX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
Python:
import os
import requests
resp = requests.post(
"https://api.fluxrouter.ai/v1/fetch",
headers={"Authorization": f"Bearer {os.environ['FLUX_API_KEY']}"},
json={"url": "https://example.com"},
)
markdown = resp.json()["markdown"]
print(markdown)
That's it. The markdown key is the page, already converted. You feed it to a model.
Request fields
| Field | Required | What it does |
|---|---|---|
url | Yes | The page to fetch. Must be an http or https URL. No default. |
render | No | Boolean, default false. Set true to render the page's JavaScript before extracting the markdown. For single-page apps and pages that paint their content via JS. |
Most pages need only url. Reach for render: true when a plain fetch comes back thin or empty, which usually means the real content only shows up after the browser runs the page's JavaScript. Dashboards and single-page apps are the classic case. Rendered fetches cost more, so it's a flag you flip on purpose, not a default.
What comes back
A successful fetch returns JSON with three keys:
{
"markdown": "...",
"format": "markdown",
"url": "https://example.com"
}
markdown is the page as markdown. format tells you the content format, and right now it's always markdown. url is the URL that got fetched.
What the shape doesn't show is what the markdown itself looks like, so I fetched a real one: the Wikipedia article on Markdown. It came back as about 71,000 characters of clean markdown, and it opens with a small metadata block before the body:
Title: Markdown - Wikipedia
URL Source: https://en.wikipedia.org/wiki/Markdown
Published Time: Wed, 15 Jul 2026 16:54:43 GMT
Markdown Content:
... the article body, as markdown ...
So you get a titled, sourced, timestamped document, not a tag-soup dump. That preamble earns its keep when you feed the page to a model, because it labels where the context came from.
Now that it's wired
The call works. Here's what to actually do with it. All copy-paste, all runnable.
1. Ground a model on a live page. Fetch the URL, then pass the returned markdown as context in a chat completion. Same key. Now the model answers from what the page says today, not from whatever was in its training data.
import os
import requests
from openai import OpenAI
FLUX = os.environ["FLUX_API_KEY"]
page = requests.post(
"https://api.fluxrouter.ai/v1/fetch",
headers={"Authorization": f"Bearer {FLUX}"},
json={"url": "https://example.com/pricing"}, # swap in the real page you want grounded
).json()["markdown"]
client = OpenAI(base_url="https://api.fluxrouter.ai/v1", api_key=FLUX)
answer = client.chat.completions.create(
model="flux-standard",
messages=[
{"role": "system", "content": "Answer only from the page below."},
{"role": "user", "content": f"What plans are listed?\n\n{page}"},
],
)
print(answer.choices[0].message.content)
2. Prep a URL for RAG. The part fetch saves you is turning a live page into clean, chunkable text, since markdown splits far cleaner than raw HTML. Embedding and storage stay your call, any embedder, any vector DB.
import os
import requests
def fetch_markdown(url):
resp = requests.post(
"https://api.fluxrouter.ai/v1/fetch",
headers={"Authorization": f"Bearer {os.environ['FLUX_API_KEY']}"},
json={"url": url},
)
return resp.json()["markdown"]
md = fetch_markdown("https://example.com/docs")
chunks = [md[i:i + 2000] for i in range(0, len(md), 2000)] # naive split; use a markdown-aware splitter for real work
# hand `chunks` to your embedder + vector DB from here
print(f"{len(chunks)} chunks ready to embed")
3. Try plain first, fall back to render. Rendering costs 4x, so don't reach for it by default. Fetch plain, and only retry with render: true when the page comes back suspiciously thin, which is the tell that the content gets painted by JavaScript.
import os, requests
def fetch(url, render=False):
return requests.post(
"https://api.fluxrouter.ai/v1/fetch",
headers={"Authorization": f"Bearer {os.environ['FLUX_API_KEY']}"},
json={"url": url, "render": render},
).json()["markdown"]
md = fetch("https://app.example.com/status")
if len(md) < 200: # thin body = probably a JS-painted page
md = fetch("https://app.example.com/status", render=True)
print(md)
4. Watch a page for changes. Fetch on a schedule, compare against last run, and show exactly what moved. Good for a competitor's pricing page, a changelog, a docs page you depend on. Markdown diffs cleanly, which is the real payoff over diffing raw HTML.
import os, difflib, pathlib, requests
STATE = pathlib.Path("last_page.md")
def fetch(url):
return requests.post(
"https://api.fluxrouter.ai/v1/fetch",
headers={"Authorization": f"Bearer {os.environ['FLUX_API_KEY']}"},
json={"url": url},
).json()["markdown"]
new = fetch("https://example.com/changelog")
old = STATE.read_text() if STATE.exists() else ""
changes = [l for l in difflib.unified_diff(old.splitlines(), new.splitlines(), lineterm="")
if l[:1] in "+-" and not l.startswith(("+++", "---"))]
if changes:
print("changed:")
print("\n".join(changes[:40])) # the added/removed lines: feed these to your alert
STATE.write_text(new) # remember this run for next time
5. Batch a list of URLs. Loop, collect the markdown, and handle per-request failures as they come. Anything that isn't a 200 gets skipped with its status logged, so one bad page never sinks the run. A 502 is the common one: that page couldn't be pulled, so move on.
import os, time, requests
urls = ["https://example.com", "https://example.org", "https://example.net"]
out = {}
for url in urls:
r = requests.post(
"https://api.fluxrouter.ai/v1/fetch",
headers={"Authorization": f"Bearer {os.environ['FLUX_API_KEY']}"},
json={"url": url},
)
if r.status_code == 200:
out[url] = r.json()["markdown"]
else:
print(f"skip ({r.status_code}):", url) # e.g. 502, that page couldn't be fetched
time.sleep(0.5) # gentle between requests
print(f"got {len(out)} of {len(urls)} pages")
Deeper stuff, a full research agent or a proper RAG pipeline on Flux, is its own piece. This gets you fetching pages today.
Costs, scope, and errors
Billing is per fetch, and it lands on the same Flux bill as everything else. One invoice, nothing separate to reconcile.
Per the guide, as of July 16, 2026:
| Fetch type | Price |
|---|---|
Standard (no render) | $0.005 per fetch |
Rendered (render: true) | $0.02 per fetch |
Those are the published list prices from the fetch guide, and pricing in this space moves, so treat the live guide as the number that counts on any given day. A few things the guide is clear on and worth coding around: you're only charged for successful fetches, so blocked URLs and failed fetches aren't billed. And repeated fetches of the same URL within a short window are served from cache and not re-billed.
What you can point it at: public http and https pages. Private, loopback, and link-local addresses get rejected. It won't log into sites or pull authenticated content, mass-scraping the big social platforms isn't supported, and it always hands back markdown, because the whole point is feeding a model's context, not mirroring raw HTML.
And the error map, so your client handles the common cases instead of guessing:
| Code | Means | Do this |
|---|---|---|
400 | Invalid or empty JSON body, missing url, or a blocked / non-public target URL | Fix the body or the URL. |
401 | Key missing or invalid | Fix the key. |
402 | Valid key, no paid plan (the key isn't eligible yet) | Use a key on a paid plan. |
502 | The page couldn't be fetched | Retry, or check the URL. |
503 | Render tier temporarily unavailable | Retry, or drop render and take the plain fetch. |
One thing this piece is not about, so you don't go looking for it here: having a model search the web and answer with citations. That's grounded web search, a separate capability. This is the other job, fetching one page you already know the URL of.
Receipts
Every claim here traces to a public surface. Run the calls yourself; the live API and the live guide are the final word.
FluxRouter (published docs and live endpoint):
- The
POST /v1/fetchendpoint, theurlandrenderfields, the{ markdown, format, url }response, the per-fetch prices, the billing behavior, the blocked-address scope, the error table, and the paid-plan gate: Web fetch guide - Which plans include web fetch: Pricing
- The documented error codes (
400,401,402,502,503) and what each returns: the web fetch guide - Getting a key and your first call: Quickstart
Ready to wire it up? Grab a key from the quickstart, open the web fetch guide, and remember web fetch runs on a paid plan.
