Documentation
Three ways to find pages, six ways to get them back out. Everything below is available over HTTP with no account and no API key, from the command line, or as an MCP server inside Claude.
The mode decides how pages are found, not how they are extracted — extraction is the same either way. If you are not sure which to use, probe the site first and take the recommendation.
normal --depth N Any site. The fallback when there is nothing better.
Starts at the URL you give it and walks internal links breadth-first, one depth level at a time. Only reaches pages something links to — orphan pages stay invisible.
sitemap --sitemap Static sites, and anywhere with orphan pages.
Reads robots.txt, then every common sitemap location. Follows sitemap indexes and handles gzipped .xml.gz sitemaps. Usually the most complete option for a non-WordPress site.
wordpress --wordpress WordPress sites with an open REST API.
Enumerates published content straight through /wp-json/wp/v2/. Several times faster, because there is no HTML to parse. Falls back to fetching the live page when a page builder leaves content.rendered empty.
All three fetch concurrently and crawl breadth-first, one depth level at a time.
Every fetched page goes through the same pipeline, whatever mode found it.
Two URLs that render the same page are normalised to the same key so the same content is never crawled twice: the scheme and host are lowercased, fragments and tracking parameters are dropped, remaining parameters are sorted, and index files and trailing slashes are stripped.
That normalised form is used for deduplication only. The URL actually requested is the one the site advertises — sitemap entries and links are fetched exactly as written. This matters on sites that canonicalise to a trailing slash, where requesting the stripped form would earn a redirect that misses the CDN and doubles the number of requests.
Four endpoints, no authentication. POST bodies are JSON. Every response carries a
success boolean, and
failures carry an error string.
/api/probe.php Call this first. Inspects a site before you commit to crawling it: does it publish a sitemap, is the WordPress REST API open, and how many pages would each strategy reach. Returns a recommendedMode you can pass straight to crawl.php.
| Parameter | Type | Default | Description |
|---|---|---|---|
url
req
| string | — | The site to inspect. Must be http or https. |
/api/crawl.php Starts a job, returns immediately. Launches a background crawl and hands back a job_id. The crawl keeps running server-side whether or not you keep polling.
| Parameter | Type | Default | Description |
|---|---|---|---|
url
req
| string | — | The site to crawl. |
crawl_mode | string | "normal" | One of "normal", "sitemap", "wordpress". Anything else silently falls back to "normal". |
max_pages | integer | 150 | Clamped to 1–1000. Pass 0 for the 1000-page maximum. |
depth | integer | 2 | Link levels to follow, clamped to 1–5. Ignored in wordpress mode. |
workers | integer | 5 | Concurrent fetches, clamped to 1–10. |
wp_types | string | "posts,pages" | WordPress mode only. Comma-separated REST bases, or "all". |
include_paths | string | "" | Restrict the crawl to these path prefixes, e.g. "/blog,/guides". |
exclude_paths | string | "" | Skip these paths, e.g. "/tag,/author". |
include_subdomains | boolean | false | Follow links onto subdomains of the same site. |
respect_robots | boolean | false | Obey robots.txt disallow rules. |
/api/status.php Poll until it is done. Returns live progress while the crawl runs, then the finished result plus a saved_as filename. The result is delivered once — poll again after that and the job reads as expired (404).
| Parameter | Type | Default | Description |
|---|---|---|---|
job_id
req
| string | — | The job_id returned by crawl.php. |
/api/export.php Re-render without re-crawling. Renders a completed crawl in any output format. Completed crawls are saved server-side, so switching format costs nothing — the site is not fetched again.
| Parameter | Type | Default | Description |
|---|---|---|---|
file
req
| string | — | A filename previously returned as saved_as. Anything else is rejected. |
format | string | "markdown" | Any of the six output formats. |
inline | integer | — | Pass 1 to render in the browser instead of downloading. |
Probe, start, poll, export. The crawl runs detached — a dropped poll does not stop it, and you can pick the job back up with the same job_id until it is delivered.
curl -X POST https://crawller.dev/api/probe.php \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}' # 1. start the job
curl -X POST https://crawller.dev/api/crawl.php \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com","crawl_mode":"sitemap","max_pages":200}'
# → {"success":true,"job_id":"crawl_6a94...","mode":"sitemap","crawls_remaining":11}
# 2. poll it
curl "https://crawller.dev/api/status.php?job_id=crawl_6a94..."
# 3. re-render the finished crawl in any format
curl "https://crawller.dev/api/export.php?file=example.com-2026-08-30_120000-a1b2c3.json&format=llms-full&inline=1" {
"success": true,
"state": "running",
"progress": {
"state": "running",
"crawled": 84,
"discovered": 397,
"max_pages": 200,
"current": "https://example.com/pricing/"
}
} {
"success": true,
"state": "complete",
"saved_as": "example.com-2026-08-30_120000-a1b2c3.json",
"data": {
"summary": {
"startUrl": "https://example.com/",
"baseDomain": "example.com",
"totalPages": 120,
"successPages": 120,
"errorPages": 0,
"emptyPages": 0,
"discoveredUrls": 397,
"maxDepth": 1,
"crawlMode": "sitemap",
"workers": 5,
"crawlTime": "18.6s",
"timestamp": "2026-08-30T12:00:00.000000",
"usedSitemap": true,
"truncated": true
},
"pages": [
{
"url": "https://example.com/pricing/",
"title": "Pricing",
"description": "Simple per-seat pricing.",
"content": "## Pricing\n\nSimple per-seat pricing...",
"wordCount": 612,
"extraction_method": "trafilatura",
"metadata": { "canonical": "...", "openGraph": {} },
"schema": { "jsonLd": [], "microdata": [] },
"links": { "internal": [], "external": [] },
"error": null
}
]
}
}
Set on the crawler with --format,
or applied after the fact by export.php. Re-rendering a saved crawl never re-fetches the site.
| Format | What you get |
|---|---|
json | The full crawl object — every page with metadata, schema, and extracted links. The default. |
clean-json | The same crawl with the noise dropped: URL, title, description, and content only. |
markdown | One markdown document for the whole site, with a header block and each page as a section. |
llms | An llms.txt index — one titled, described link per page. Small enough to paste anywhere. |
llms-full | llms.txt with the full page text inlined. The one to use when the result goes straight into a prompt. |
text | Plain text, no markup at all. |
The public API is unauthenticated, so it is rate limited per IP. Values a request exceeds are clamped rather than rejected.
crawl.php returns a crawls_remaining count with every job so you can see where you stand. Private and reserved IP addresses are refused.
The crawler runs standalone with no PHP and no web server —
pyScripts/site_crawler.py.
It needs trafilatura, beautifulsoup4, requests and lxml_html_clean.
# what will reach the most pages?
python pyScripts/site_crawler.py https://example.com --probe -p
# whole site from the sitemap, unlimited pages, 8 workers
python pyScripts/site_crawler.py https://example.com --sitemap --max-pages 0 --workers 8
# WordPress, every public post type
python pyScripts/site_crawler.py https://example.com --wordpress --wp-types all
# straight to llms-full on disk
python pyScripts/site_crawler.py https://example.com --sitemap --format llms-full -o site.txt
# one page, markdown only
python pyScripts/site_crawler.py https://example.com/about --single --markdown
# re-render an existing crawl without touching the network
python pyScripts/site_crawler.py --render-file site.json --format markdown | Flag | Default | Description |
|---|---|---|
--depth, -d | 2 | Max link depth in normal mode. |
--max-pages, -m | 200 | Page cap. 0 or negative means unlimited. |
--workers, -w | 5 | Concurrent fetches. |
--delay | 0.0 | Per-request pause, in seconds. Applied per worker, so pair it with --workers 1 to actually slow the crawl down. |
--timeout | 25 | Per-request timeout in seconds. |
--retries, -r | 3 | Attempts per URL before the page is given up on. |
--sitemap | off | Seed from robots.txt and sitemap.xml instead of following links. |
--wordpress | off | Pull content through the WordPress REST API. |
--wp-types | posts,pages | WordPress REST bases to fetch, or "all". |
--single, -s | off | One page, no link following. |
--probe | off | Report which mode reaches the most pages, then exit. |
--format, -f | json | Output format. Any of the six below. |
--render-file | — | Re-render an existing crawl JSON instead of crawling. |
--include, -i | — | Restrict to a path prefix. Repeatable. |
--exclude, -e | — | Skip matching URLs. Repeatable. |
--include-subdomains | off | Follow links onto subdomains of the same site. |
--respect-robots | off | Obey robots.txt disallow rules. |
--user-agent | CrawllBot/2.0 | Override the User-Agent. |
--output, -o | stdout | Write to a file instead of stdout. |
--pretty, -p | off | Pretty-print JSON output. |
Want this inside Claude instead? Set up the MCP server, or try a crawl in the browser.