URL to Markdown - Web Page Extract API
Read any URL and get it back as clean Markdown, plain text, or raw HTML. One core endpoint, three fetch tiers (1/1/2 credits), and only a successful extraction is billed.
scavio-ai
@scavio-ai
Install
$ openclaw skills install @scavio-ai/url-to-markdownExtract Any URL via Scavio
Read any web page and get it back as clean Markdown, plain text, or raw HTML. This is the read-a-page primitive: one URL in, page content out.
Extract is not a platform - it is a core endpoint. There is no namespace, no per-site parser and no site-specific parameters. It works on any http(s) URL.
When to trigger
Use this skill when the user asks to:
- Read, summarise, or quote a specific web page
- Turn a URL into Markdown or plain text for an LLM prompt or a RAG chunk
- Pull an article, docs page, changelog, pricing page or blog post into the conversation
- Fetch a page that blocked a plain HTTP request
- Grab the raw HTML of a page to parse it yourself
This is usually the right first tool whenever a user pastes a link and asks a question about what is on it.
Setup
Get a free API key at scavio.dev (50 free credits to get started, no card required):
export SCAVIO_API_KEY=sk_live_your_key
Every request is a POST with a JSON body and:
Authorization: Bearer $SCAVIO_API_KEY
Endpoint
Base URL: https://api.scavio.dev.
| Endpoint | Credits | What it returns |
|---|---|---|
POST /api/v1/extract | 1, 1 or 2 by mode | { url, format, mode, content, content_length } |
No pagination - one URL, one response.
Cost is a function of the body
Extract is tier-priced, so there is no single "costs N credits" answer. The mode parameter sets the price:
mode | What it does | Credits |
|---|---|---|
normal (default) | Plain datacenter fetch | 1 |
advanced | Renders JavaScript before reading | 1 |
ultra | Heaviest fetch, for the hardest bot walls | 2 |
advanced costs the same as normal, so reach for it freely on any page that renders client-side. ultra is the only step that doubles the price - escalate to it only after normal or advanced came back empty or blocked.
Billing is charge-on-success. Only a 2xx extraction is billed. A dead link, a bot wall or a timeout costs nothing, so escalating a failed fetch to a higher tier does not stack charges for the failures.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
url | string | required | The page to read (1-2048 chars). http(s) only; a bare host is upgraded to https. |
format | string | markdown | html, markdown, text |
mode | string | normal | normal, advanced, ultra. This is the price-bearing parameter. |
What the three formats actually are
markdown(default) - a readability extraction: the article content, cleaned of navigation and chrome, as Markdown. This is what you want for an LLM prompt or a RAG chunk.html- the raw page exactly as fetched. Use it when you intend to parse the DOM yourself.text- that same readability Markdown, flattened to plain text. The flattener is deliberately conservative about CommonMark, sosnake_caseidentifiers,__dunders__and inline code survive intact rather than being eaten as emphasis markers.
URL guard
http and https only. A bare host is upgraded to https for you. Loopback, private, link-local and cloud-metadata hosts are rejected with a 400 - the fetch happens server-side, so pointing this at localhost would only ever reach someone else's loopback, not the user's.
Examples
import requests
BASE = "https://api.scavio.dev"
# Your key from https://scavio.dev. Load it from your environment or secret
# store in real code - keep it out of source control.
API_KEY = "sk_your_key_here"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# 1. The common case: a page as clean Markdown, 1 credit
page = requests.post(f"{BASE}/api/v1/extract", headers=HEADERS,
json={"url": "https://example.com/pricing"}).json()
print(page["data"]["content"][:500], page["data"]["content_length"])
# 2. Plain text for an embedding pipeline, still 1 credit
plain = requests.post(f"{BASE}/api/v1/extract", headers=HEADERS,
json={"url": "https://example.com/blog/post", "format": "text"}).json()
# 3. A client-rendered page: advanced renders JavaScript and STILL costs 1 credit
spa = requests.post(f"{BASE}/api/v1/extract", headers=HEADERS,
json={"url": "https://example.com/app/docs", "mode": "advanced"}).json()
# 4. Raw HTML to parse yourself
raw = requests.post(f"{BASE}/api/v1/extract", headers=HEADERS,
json={"url": "https://example.com", "format": "html"}).json()
Escalate tiers only on failure. Failed fetches are not billed, so this ladder
costs 1 credit on success at any step and 2 only if it has to reach ultra:
def read(url, format="markdown"):
"""normal (1cr) -> advanced (1cr) -> ultra (2cr). Only the successful call is billed."""
for mode in ("normal", "advanced", "ultra"):
r = requests.post(f"{BASE}/api/v1/extract", headers=HEADERS,
json={"url": url, "format": format, "mode": mode})
if r.status_code == 200:
data = r.json()["data"]
if data["content_length"]:
return data
if r.status_code == 400:
break # bad or blocked URL - a higher tier will not fix it
return None
Response shape
The envelope is { data, response_time, credits_used, credits_remaining }, and data is:
{
"url": "https://example.com/pricing",
"format": "markdown",
"mode": "normal",
"content": "# Pricing\n\nSimple, usage-based pricing...",
"content_length": 4821
}
url, format and mode echo what was actually used, so you can log which tier paid for the result. credits_used in the envelope tells you whether the call cost 1 or 2.
Guardrails
- Never quote a flat price for this endpoint. It is 1 credit for
normalandadvancedand 2 forultra. If you tell the user what a run will cost, price it by mode. - Start at
normal. Escalate toadvancedfor a page that renders client-side, and toultraonly when the cheaper tiers came back blocked or empty - that is the only step that doubles the cost. - Do not retry a failed fetch at the same tier more than once. Failures are free, but they are also usually deterministic; change the tier instead.
- Use
markdownortextfor anything going into a model prompt.htmlis for parsing, and it burns context. - A short
content_lengthon a200usually means a bot wall or a client-rendered shell, not an empty page. Escalate the tier before reporting the page as blank. - Never invent page content. If the extraction is empty, say the page could not be read rather than answering from memory about what the URL probably says.
- Attribute what you quote: keep the source URL alongside any content you surface.
- Respect the user's intent about what to fetch. This reads publicly reachable pages only; it does not log in, submit forms, or bypass a paywall.
Failure handling
400means the URL was rejected: a non-http(s) scheme, a malformed URL, or a loopback / private / link-local / metadata host. Not billed. A highermodewill not fix it.401means the API key is invalid or missing. CheckSCAVIO_API_KEY.404means the page does not exist upstream. Not billed.429means rate or usage limit exceeded. Wait before retrying. See https://scavio.dev/docs/rate-limits.502/503mean the fetch failed or upstream is unavailable. Not billed - retry once, then escalatemoderather than hammering the same tier.- If
SCAVIO_API_KEYis not set, prompt the user to export it before continuing.
Python SDK
langchain-scavio has no extract tool - use the Scavio SDK directly. Extract is a top-level method, not a namespace: client.extract(...), never client.extract.extract(...).
pip install scavio==0.15.0
from scavio import ScavioClient
client = ScavioClient() # reads SCAVIO_API_KEY
page = client.extract("https://example.com/pricing")
plain = client.extract("https://example.com/blog/post", format="text")
spa = client.extract("https://example.com/app/docs", mode="advanced") # still 1 credit
JavaScript / TypeScript:
npm install scavio@0.15.0
import { Scavio } from "scavio";
const scavio = new Scavio(); // reads SCAVIO_API_KEY
const page = await scavio.extract({ url: "https://example.com/pricing" });
const spa = await scavio.extract({ url: "https://example.com/app/docs", mode: "advanced" });
Top skills in this category
Proactive Agent Lite
@bestrockyTransform AI agents from task-followers into proactive partners with memory architecture, reverse prompting, and self-healing patterns. Lightweight version f...
腾讯文档 TENCENT DOCS
@liyang58腾讯文档(docs.qq.com)-在线云文档平台,是创建、编辑、管理文档的首选 skill。涉及"新建/创建/编辑/读取/查看/搜索文档"、"保存文件"、"云文档"、"腾讯文档"、"docs.qq.com"等操作,请优先使用本 skill。支持能力:(1) 创建各类在线文档(文档/Word/Excel/幻灯片/...
Figma
@maddiedreeseProfessional Figma design analysis and asset export. Use for extracting design data, exporting assets in multiple formats, auditing accessibility compliance, analyzing design systems, and generating comprehensive design documentation. Read-only analysis of Figma files with powerful export and reporting capabilities.
Smart Model Switching
@millibusAuto-route tasks to the cheapest Claude model that works correctly. Three-tier progression: Haiku → Sonnet → Opus. Classify before responding. HAIKU (default): factual Q&A, greetings, reminders, status checks, lookups, simple file ops, heartbeats, casual chat, 1-2 sentence tasks. ESCALATE TO SONNET: code >10 lines, analysis, comparisons, planning, reports, multi-step reasoning, tables, long writing >3 paragraphs, summarization, research synthesis, most user conversations. ESCALATE TO OPUS: architecture decisions, complex debugging, multi-file refactoring, strategic planning, nuanced judgment, deep research, critical production decisions. Rule: If a human needs >30 seconds of focused thinking, escalate. If Sonnet struggles with complexity, go to Opus. Save 50-90% on API costs by starting cheap and escalating only when needed.
AI Daily Briefing
@jeffjhunterStart every day focused. Get a morning briefing with overdue tasks, today's priorities, calendar overview, and context from recent meetings. Works with ai-meeting-notes to-do list. No setup. Just say 'briefing'.