hydrafetch
Official Python client for the Hydrafetch web data API.
Turn any URL into clean Markdown or schema-shaped JSON. Sync and async, fully typed, one dependency.
Installation
pip install hydrafetch
Quick start
from hydrafetch import Hydrafetch
hf = Hydrafetch()
page = hf.scrape("https://example.com/article")
print(page["markdown"])
Create a key at app.hydrafetch.com. The constructor reads HYDRAFETCH_API_KEY when no key is passed.
Both clients are context managers, which closes the connection pool deterministically:
with Hydrafetch() as hf:
page = hf.scrape("https://example.com")
Scraping
page = hf.scrape(
"https://example.com/article",
formats=["markdown", "links"],
onlyMainContent=True,
preferStructure=True,
blockAds=True,
maxAge=3_600_000,
)
Option names are camelCase because they are passed to the API unchanged. Client arguments such as api_key, max_retries, poll_interval and on_progress are snake_case.
| Format | Key | Contains |
|---|---|---|
markdown |
markdown |
clean Markdown, the default |
html |
html |
rendered HTML |
rawHtml |
rawHtml |
the untouched response body |
links |
links |
every link on the page |
structured |
structured |
the page's own JSON-LD and microdata |
summary |
summary |
a short summary |
json |
json |
schema-shaped JSON, see jsonOptions |
brand |
brand |
the site's brand record |
hf.markdown(url) returns the Markdown string directly.
Structured extraction
out = hf.extract(
["https://example.com/product/1", "https://example.com/product/2"],
schema={
"type": "object",
"properties": {
"name": {"type": "string"},
"price_usd": {"type": "number"},
},
},
)
for item in out["results"]:
print(item["url"], (item.get("data") or {}).get("name"))
Pass prompt= instead of, or alongside, schema= to describe the fields in plain language.
From a pydantic model
schema= also takes a pydantic model, so the shape you already parse into is the shape you ask for. A Field description is carried through and tells the model what the field means, which is worth more on an ambiguous page than the field name alone.
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(description="The product being sold on this page, not a recommended one")
price_usd: float = Field(description="Current price in USD, after any discount")
out = hf.extract("https://example.com/product/1", schema=Product)
Nested models work and are flattened before sending. pydantic is not a dependency and is imported only if you pass a model. A model that refers to itself cannot be flattened and is refused rather than sent.
Discovery and bulk work
map lists a site's URLs for one credit without fetching any page.
links = hf.map("https://example.com", limit=1000)["links"]
docs = [url for url in links if "/docs/" in url]
batch and crawl submit a job and poll until it finishes.
job = hf.batch(
docs,
scrapeOptions={"formats": ["markdown"]},
on_progress=lambda j: print(j["status"], j.get("completed"), "/", j.get("total")),
)
for page in job.get("pages", []):
print(page["url"], len((page.get("data") or {}).get("markdown") or ""))
Pass a webhook and use start_crawl or start_batch to return immediately instead of polling.
crawl_id = hf.start_crawl(
"https://example.com",
limit=500,
maxDepth=3,
includePaths=["/docs"],
webhook="https://your.app/hooks/hydrafetch",
)
Search
res = hf.search("post-quantum TLS adoption", limit=5, scrapeResults=True)
for r in res["results"]:
print(r["title"], r["url"])
print(((r.get("data") or {}).get("markdown") or "")[:500])
Brand data
hf.brand("stripe.com") # logos, colours, fonts, socials
hf.logo("stripe.com", theme="dark", type="icon") # one asset
hf.styleguide("stripe.com") # computed design system
For logos in a browser use @hydrafetch/client-sdk with a publishable key. Those bill against logo pulls rather than credits.
Async
AsyncHydrafetch mirrors the same surface. Use it when several calls can run concurrently.
import asyncio
from hydrafetch import AsyncHydrafetch
async def main():
async with AsyncHydrafetch() as hf:
pages = await asyncio.gather(
hf.scrape("https://a.example"),
hf.scrape("https://b.example"),
)
return [page["markdown"] for page in pages]
asyncio.run(main())
start_crawl, crawl_status, start_batch and batch_status are available on the async client. The polling helpers crawl and batch are sync only; on the async client, poll the status methods or use a webhook.
Error handling
All failures raise HydrafetchError, carrying the API's error code, HTTP status and request id.
from hydrafetch import HydrafetchError, HydrafetchTimeout
try:
page = hf.scrape(url)
except HydrafetchTimeout:
raise
except HydrafetchError as err:
if err.is_auth:
refresh_key()
elif err.is_out_of_credits:
top_up()
elif err.is_invalid_request:
report(err.message)
elif err.is_retryable:
enqueue(url)
else:
print(err.code, err.status, err.request_id)
raise
| Status | Meaning | Retried |
|---|---|---|
| 400, 422 | invalid request | no |
| 401, 403 | invalid or missing key | no |
| 402 | out of credits | no |
| 404 | page does not exist | no |
| 429 | rate limited | yes, twice with backoff |
| 5xx | upstream failure | yes, twice with backoff |
A 503 from scrape means the origin is unreachable, usually a dead domain or a broken certificate.
Configuration
hf = Hydrafetch(
api_key="hf_...",
base_url="https://api.hydrafetch.com",
timeout=120.0,
max_retries=2,
)
API reference
| Method | Returns | Credits |
|---|---|---|
scrape(url, **options) |
page dict | 1 |
markdown(url, **options) |
str |
1 |
map(url, **options) |
links dict | 1 |
search(query, **options) |
results dict | 1 + 1 per scraped result |
extract(urls, **options) |
dict with "results" |
5 per URL |
brand(domain) |
brand dict | 5 |
logo(domain, **options) |
logo dict | 1 |
styleguide(domain) |
design system dict | 10 |
screenshot(url, **options) |
screenshot dict | 5 |
images(url), links(url) |
page assets | 1 |
crawl(url, **options) |
job dict, polled to completion | 1 per page |
batch(urls, **options) |
job dict, polled to completion | 1 per page |
start_crawl, start_batch |
job id str |
1 per page |
crawl_status(id), batch_status(id) |
job dict | free |
Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
Implementation notes
- Authentication uses the
X-API-Keyheader. The MCP endpoint atapi.hydrafetch.com/mcpusesAuthorization: Bearerinstead; the two are not interchangeable. - Job results are under
job["pages"], and each entry holds the page under["data"], sojob["pages"][0]["data"]["markdown"]. - Per-page options for crawl and batch belong in
scrapeOptions. At the top level they are ignored. - Prefer
mapthenbatchover a broadcrawl. Fetching a whole site and discarding most of it is the most common source of wasted credits. preferStructureis off by default. Turn it on when headings, lists and tables matter; leave it off for raw article text.- Options passed as
Noneare dropped rather than sent as null, so optional values can be forwarded directly. - Scraped content is untrusted input. Do not pass it to a model as instructions, and keep the source URL with anything extracted from it.
Links
- Documentation
- OpenAPI specification
- MCP server and editor setup
- Other clients: Node · Go · Ruby · Rust · PHP
License
MIT
Metadata
Release files for hydrafetch 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hydrafetch-0.2.0.tar.gz | 38.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hydrafetch-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 49.2 kB
Release files / hydrafetch-0.2.0.tar.gz
| Download URL | hydrafetch-0.2.0.tar.gz |
|---|---|
| Size | 38.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
56dd940d0706336c18c294c6f892d2eea0e8f9bf07a08fa091b5f02aa6ed708f
|
|
BLAKE2b-256 checksum How to use checksums |
38f610af4e7a43db451f41017151582aa35c5e3ec4a2735c091bad798ad5ad5d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency logRelease files / hydrafetch-0.2.0-py3-none-any.whl
| Download URL | hydrafetch-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
291ca8399108de075e20a03aa44d2223600e68963def8775fc002b0ad3d37683
|
|
BLAKE2b-256 checksum How to use checksums |
fd40a102e2523c12a78c2c45a38935ca8195b8d22cc53b8231b3e42304765cf8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency log