# CRW — open-source web scraper & crawler (Firecrawl-compatible)

> Docs at `docs.fastcrw.com`. Hosted API at `https://api.fastcrw.com`. Self-hosted at `http://localhost:3000`. Five endpoints: `/v1/search`, `/v1/scrape`, `/v1/crawl`, `/v1/map`, `/v1/extract`.

## Quick: copy this and run

Auth (hosted only): `Authorization: Bearer $CRW_API_KEY`. Self-hosted (`http://localhost:3000`) needs no header.

Body: `{ "query": string, "limit": number (default 5, max 20), "lang"?: "en", "tbs"?: "qdr:d|w|m|y", "sources"?: ["web"|"news"|"images"] }`

### Python (stdlib only — defaults to hosted, fails loudly if key is missing)

    #!/usr/bin/env python3
    """Search CRW and print top results as JSON."""
    import json, os, sys, urllib.request

    API_KEY = os.environ.get("CRW_API_KEY")
    # Default to the hosted API. Self-hosting? Set CRW_API_URL=http://localhost:3000
    BASE = os.environ.get("CRW_API_URL", "https://api.fastcrw.com")
    URL = f"{BASE}/v1/search"

    headers = {"Content-Type": "application/json"}
    if API_KEY:
        headers["Authorization"] = f"Bearer {API_KEY}"
    elif BASE.startswith("https://api.fastcrw.com"):
        sys.exit("error: CRW_API_KEY is required for the hosted API. "
                 "Get one at https://fastcrw.com (500 free credits) "
                 "or set CRW_API_URL=http://localhost:3000 to self-host.")

    req = urllib.request.Request(
        URL,
        data=json.dumps({"query": "renewable energy trends 2024", "limit": 3}).encode(),
        headers=headers,
        method="POST",
    )
    with urllib.request.urlopen(req) as r:
        body = json.loads(r.read())  # body == {"success": true, "data": [...]}

    # Results live under body["data"]. Each row has `title`, `url`, `snippet`,
    # `description`, `position`, `score`, `category` — print the LLM-ready subset.
    print(json.dumps(
        [{"title": x["title"], "url": x["url"], "snippet": x["snippet"]}
         for x in body["data"]],
        indent=2,
    ))
    # Run with: python3 search.py  (file is python3-only — no `python` alias needed)

### cURL

    curl -X POST https://api.fastcrw.com/v1/search \
      -H "Authorization: Bearer $CRW_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"query":"renewable energy trends 2024","limit":3}'

### CLI (one-shot LLM-ready output)

The `crw` CLI projects directly to the title/url/snippet schema with `--json --fields` — no `jq` reshape needed:

    # Install: brew install us/crw/crw  (or: cargo install crw-cli)
    crw search "renewable energy trends 2024" --json --fields title,url,snippet --limit 3

    # Available fields: title, url, description, snippet, position, score, category
    # --json is shorthand for --format json (also accepts --format text|markdown)
    # Self-hosted: no auth. Hosted: export CRW_API_KEY=crw_live_...

### Response shape

    {
      "success": true,
      "data": [
        {"url": "https://...", "title": "...",
         "description": "...", "snippet": "...",
         "position": 1, "score": 9.5}
      ]
    }

## Other endpoints

- `POST /v1/scrape` `{"url":"...","formats":["markdown"]}` — one URL → markdown
- `POST /v1/map` `{"url":"...","limit":50}` — discover URLs
- `POST /v1/crawl` `{"url":"...","maxPages":5}` — async multi-page; poll `GET /v1/crawl/{id}`
- `POST /v1/scrape` with `formats:["json"]` + `jsonOptions.schema` — structured extract

## Install (local setup only)

`pip install crw` · `npx crw-mcp` · `brew install us/crw/crw` · `curl -fsSL https://raw.githubusercontent.com/us/crw/main/install.sh | sh` · `docker run -p 3000:3000 ghcr.io/us/crw:latest`

## Resources

- OpenAPI spec
- llms.txt
- Context7
- MCP Server (npm)
- Typed Python SDK
- Agent Skills (SKILL.md)
- CLI (Homebrew tap)
- GitHub

More API

# Map

Discover the URLs a site exposes before you scrape or crawl it. Map is the lightest way to understand scope without paying the cost of a full multi-page extraction job.

Best for: site discovery
Returns: links only
Start with: sitemap on, low depth
View Crawl View Scrape
Try it in the Playground
Inspect reachability before you recurse
Use one root URL, keep `maxDepth` small, and inspect the discovered links. If the map is wrong, the crawl will be wrong too.
Open Playground Jump to Crawl

## Mapping a site with CRW

### /v1/map

    POST /v1/map

Authentication:

- Hosted: send `Authorization: Bearer YOUR_API_KEY`
- Self-hosted: only required when `auth.api_keys` is configured

### Installation

Map is also a plain HTTP route. No dedicated SDK is required.

### Basic usage

Start with this request:

    {
      "url": "https://example.com",
      "maxDepth": 1,
      "useSitemap": true
    }

:::tabs ::tab{title="Python"}

    import requests

    resp = requests.post(
        "https://fastcrw.com/api/v1/map",
        headers={"Authorization": "Bearer YOUR_API_KEY"},
        json={
            "url": "https://example.com",
            "maxDepth": 1,
            "useSitemap": True,
        },
    )

    print(resp.json()["data"]["links"])

::tab{title="Node.js"}

    const resp = await fetch("https://fastcrw.com/api/v1/map", {
      method: "POST",
      headers: {
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        url: "https://example.com",
        maxDepth: 1,
        useSitemap: true
      })
    });

    const body = await resp.json();
    console.log(body.data.links);

::tab{title="cURL"}

    curl -X POST https://fastcrw.com/api/v1/map \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com",
        "maxDepth": 1,
        "useSitemap": true
      }'

:::

### Response

    {
      "success": true,
      "data": {
        "links": [
          "https://example.com",
          "https://example.com/about"
        ]
      }
    }

## Parameters

Field | Type | Default | Description
--- | --- | --- | ---
`url` | string | required | Site or page URL to start discovery from
`maxDepth` | number | `2` | Maximum discovery depth
`useSitemap` | boolean | `true` | Read sitemap hints when available
`timeout` | number | server default | Custom timeout in seconds

## Sitemap behavior

With `useSitemap: true`, CRW uses sitemap hints when they are available. That usually makes the first discovery pass faster and more complete on structured sites.

Good default:

- keep sitemap on,
- keep depth low,
- inspect the discovered links,
- then decide whether crawl is worth it.

## When map is better than crawl

Use map when:

- you need to understand a site's shape before extracting any content,
- you want a cheap first pass over a large site,
- or you are deciding which section is worth crawling.

Use crawl only after you already trust the section you want to recurse through.

## Common production patterns

- Run map before crawl when you do not yet trust the start URL scope.
- Keep `maxDepth` low first so you can inspect the discovered section.
- Use sitemap hints when you want a faster first pass over structured sites.

## Common mistakes

- Using map when you already know the exact page and only need its content
- Expecting map to return page bodies; it only returns discovered links
- Letting depth grow before inspecting whether the discovered section is useful

## When to use something else

- Use Scrape when you want the content of a known page
- Use Crawl when you are ready to recurse through a bounded section
- Use Search when you do not know the site or page set yet

`esc`