# Create crawl

URL: https://docs.hanzo.ai/docs/openapi/crawl/read-page

> Reads one URL and answers with the page as markdown.

`POST /v1/crawl`

> [Crawl →](https://docs.hanzo.ai/docs/openapi/crawl) · [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|               |                                        |
| ------------- | -------------------------------------- |
| **Address**   | `https://api.hanzo.ai/v1/crawl`        |
| **Method**    | `POST`                                 |
| **Operation** | `read_page`                            |
| **Auth**      | `Authorization: Bearer $HANZO_API_KEY` |

Reads one URL and answers with the page as markdown.

It fetches a single URL from inside the cluster and answers with the address it
actually landed on, the document's title, its content rendered to MARKDOWN, and
whatever the page said about itself. One URL per call: batching would make the
answer a partial-failure envelope every caller then has to unpack.

A PAGE THAT COULD NOT BE FETCHED IS A NORMAL ANSWER, not a fault. An
unreachable host, a refused address and a content type that is not a document
all answer 200 with `success:false` and the reason in `error`, because the
caller sent a well-formed ask and gets a well-formed answer. Non-2xx is reserved
for a caller problem — 400 with the same body when there is no url, 401 for a
bad key, 503 when the surface is unconfigured — so error handling can trust the
status. Check `success` before reading `data`.

Admission is either a validated principal or the shared service key, presented
as X-API-Key or a Bearer; neither is refused, and an unset key fails closed
rather than opening the fetcher to the private network. Pages are archived under
the scope of the VERIFIED principal and NEVER a scope named in the body, so a
URL already read under that scope is answered from the archive without touching
the network; a service caller has no org and its pages land in the shared
corpus.

The URL is caller-supplied and dialled from INSIDE the cluster, which makes this
a request-forgery primitive by construction. Only http and https are accepted,
and every address actually dialled must be public unicast — loopback,
link-local, private and multicast are refused. The check lives in the DIALER
rather than on the hostname, because resolving a name to validate it and then
letting the transport resolve it again is a gap DNS rebinding walks straight
through; redirects re-enter the same dialer.

## Request

1 field, body `application/json` (required).

| Field | In   | Type   | Required | Description                                                                       |
| ----- | ---- | ------ | -------- | --------------------------------------------------------------------------------- |
| `url` | body | string | —        | URL is the page to read, absolute and http or https — no other scheme is dialled. |

## Response

| Status    | Body              | Meaning     |
| --------- | ----------------- | ----------- |
| `200`     | crawl.crawlResult | ok          |
| `400`     | crawl.crawlResult | bad request |
| `default` | problem-details   | refused     |

`200` body — 7 fields.

| Field           | In   | Type                | Always | Description                                                                                                                                        |
| --------------- | ---- | ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`          | body | crawl.crawlDocument | —      |                                                                                                                                                    |
| `data.markdown` | body | string              | —      | Markdown is the page's content, extracted and rendered to markdown.                                                                                |
| `data.metadata` | body | any                 | —      | Metadata is whatever the document said about itself — description, og:\*, language — plus the response status, the final URL and the content type. |
| `data.title`    | body | string              | —      | Title is the document's title, when it carried one.                                                                                                |
| `data.url`      | body | string              | —      | URL is the address actually read, after redirects.                                                                                                 |
| `error`         | body | string              | —      | Error says what stopped the fetch: the host was refused, unreachable, or served something that is not a document.                                  |
| `success`       | body | boolean             | —      | Success is whether the page was fetched and read.                                                                                                  |

Failure carries the platform error shape — see [Errors](https://docs.hanzo.ai/docs/errors).

## Examples

**CLI**

```bash
hanzo crawl create
```

**SDK**

**.ts**

```ts
import { Configuration, CrawlApi } from 'hanzoai';

const api = new CrawlApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.readPage({ url: "<url>" });
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import CrawlApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = CrawlApi(client).read_page(url="<url>")
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.CrawlAPI.ReadPage(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, crawl_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = crawl_api::read_page(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.CrawlApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new CrawlApi(client).readPage();
```

**HTTP**

```bash
curl -X POST https://api.hanzo.ai/v1/crawl \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "url": "<url>"
     }'
```

**MCP**

Tool `crawl`, op `read_page` — POST the JSON-RPC envelope to `https://api.hanzo.ai/v1/mcp`.

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "crawl",
         "arguments": {
           "op": "read_page",
           "input": {
             "url": "<url>"
           }
         }
       }
     }'
```

---

[Crawl API](https://docs.hanzo.ai/docs/openapi/crawl) · [All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
