# Batch và crawl API: đọc nhiều URL hoặc toàn bộ trang web

> Tham chiếu Batch và crawl API: đọc tới 100 URL mỗi job hoặc toàn bộ trang web tới 10.000 trang, tôn trọng robots.txt và Crawl-delay, không cần KYC.

- URL: https://webreaderapi.com/vi/docs/crawl/
- Cập nhật lần cuối: 2026-10-22
- Đơn vị phát hành: WebReaderAPI (https://webreaderapi.com)

Hai endpoint khởi chạy job chạy nền: `/batch` cho danh sách tối đa 100 URL, `/crawl` cho một trang web. Cả hai trả về một job ID; bạn thu thập các trang từ `/jobs/{job_id}`, mỗi lần một trang kết quả.

**Câu trả lời nhanh — Làm cách nào để crawl một trang web sang Markdown với WebReaderAPI?**

Gửi URL bắt đầu tới `POST https://webreaderapi.com/api/v1/crawl` với một `limit` trang. **WebReaderAPI** đọc trang web ở chế độ nền, chỉ trong cùng domain, theo sitemap, robots.txt và Crawl-delay của trang, tối đa 10.000 trang mỗi job. Thu thập các trang Markdown từ job, mỗi lần một trang kết quả. Mỗi trang đọc tốn 1 credit (2 nếu dùng trình duyệt), từ 0,07 US$/1K trang tính đến tháng 10 năm 2026. Không cần KYC; thanh toán bằng crypto.

## Tổng quan về jobs

| Thuộc tính | Giá trị |
| --- | --- |
| Batch | Tối đa 100 URL mỗi batch job: `POST https://webreaderapi.com/api/v1/batch` |
| Crawl | Crawl một website mỗi job, tối đa 10.000 trang, theo sitemap, robots.txt và Crawl-delay của trang: `POST https://webreaderapi.com/api/v1/crawl` |
| Kết quả | `GET https://webreaderapi.com/api/v1/jobs/{job_id}`, sau đó `?cursor=…` để lấy trang kết quả tiếp theo |
| Tính phí | Mỗi trang đọc được tính phí như một lệnh gọi `/read`; các trang bị robots.txt bỏ qua và các trang thất bại không tốn phí |
| Phạm vi của một lần crawl | Một domain; các liên kết tới domain khác được liệt kê nhưng không được đọc |
| Hủy job | Không có trong v1: đặt `limit` bằng số trang tối đa bạn sẵn sàng trả tiền |

## Khởi chạy job batch

`POST /api/v1/batch`

Đọc tới 100 URL ở chế độ nền với cùng các tùy chọn. Tùy chọn dùng tên và giá trị của [/read](https://webreaderapi.com/vi/docs/read/). Câu trả lời là job ID; các giá trị chỉ mang tính minh họa.

Xác thực: khóa API trong header `Authorization: Bearer API_KEY`.

**Request body**

| Field | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `urls` | array of strings | Có | 1 đến 100 URL http hoặc https. URL trùng lặp chỉ được đọc một lần. |
| `options` | object | Không | Bất kỳ tham số `/read` nào ngoại trừ `url`: `format`, `main_content`, `render`, `include_links`, `include_images`, `country`, `max_chars`, `timeout`. |

*Ví dụ: three articles as Markdown*

```json
{
  "urls": [
    "https://example.com/blog/post-1",
    "https://example.com/blog/post-2",
    "https://example.org/news/launch"
  ],
  "options": {
    "format": "markdown",
    "max_chars": 50000
  }
}
```

**Response** `202 Accepted`

```json
{
  "job_id": "job_5c1e8a90d2"
}
```

*curl*

```bash
curl -s "https://webreaderapi.com/api/v1/batch" \
  -H "Authorization: Bearer $WEBREADER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/blog/post-1", "https://example.com/blog/post-2"], "options": {"format": "markdown"}}'
```

*Python*

```python
import os
import requests

resp = requests.post(
    "https://webreaderapi.com/api/v1/batch",
    headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
    json={"urls": ["https://example.com/blog/post-1", "https://example.com/blog/post-2"], "options": {"format": "markdown"}},
    timeout=30,
)
print(resp.json()["job_id"])
```

*Node.js*

```javascript
const res = await fetch('https://webreaderapi.com/api/v1/batch', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ urls: ['https://example.com/blog/post-1', 'https://example.com/blog/post-2'], options: { format: 'markdown' } }),
});
console.log((await res.json()).job_id);
```

## Bắt đầu một job crawl

`POST /api/v1/crawl`

Đọc một trang web từ URL bắt đầu, theo chiều rộng. Trình thu thập dữ liệu ở lại trên domain của URL bắt đầu, dùng sitemap được nêu trong robots.txt hoặc tìm thấy tại `/sitemap.xml`, và tuân thủ robots.txt cùng Crawl-delay. Các giá trị chỉ mang tính minh họa.

Xác thực: khóa API trong header `Authorization: Bearer API_KEY`.

**Request body**

| Field | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `url` | string | Có | Trang bắt đầu, chẳng hạn như trang chủ của một site tài liệu. |
| `limit` | integer | Không | Số trang tối đa cần đọc, tối đa 10.000. Job dừng khi đạt số này. |
| `max_depth` | integer | Không | Số bước liên kết tối đa tính từ URL bắt đầu. |
| `include_paths` | array of strings | Không | Các mẫu đường dẫn đơn giản cần giữ, chẳng hạn như `/docs/*`. |
| `exclude_paths` | array of strings | Không | Các mẫu đường dẫn đơn giản cần bỏ qua, chẳng hạn như `/blog/tag/*`. |
| `options` | object | Không | Bất kỳ tham số `/read` nào ngoại trừ `url`, áp dụng cho mọi trang. |

*Ví dụ: the docs section of a site, 500 pages at most*

```json
{
  "url": "https://docs.example.com/",
  "limit": 500,
  "max_depth": 3,
  "include_paths": [
    "/docs/*"
  ],
  "exclude_paths": [
    "/docs/archive/*"
  ],
  "options": {
    "format": "markdown"
  }
}
```

**Response** `202 Accepted`

```json
{
  "job_id": "job_8b27f41c0e"
}
```

*curl*

```bash
curl -s "https://webreaderapi.com/api/v1/crawl" \
  -H "Authorization: Bearer $WEBREADER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://docs.example.com/", "limit": 500, "include_paths": ["/docs/*"]}'
```

*Python*

```python
import os
import requests

resp = requests.post(
    "https://webreaderapi.com/api/v1/crawl",
    headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
    json={"url": "https://docs.example.com/", "limit": 500, "include_paths": ["/docs/*"]},
    timeout=30,
)
print(resp.json()["job_id"])
```

*Node.js*

```javascript
const res = await fetch('https://webreaderapi.com/api/v1/crawl', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'https://docs.example.com/', limit: 500, include_paths: ['/docs/*'] }),
});
console.log((await res.json()).job_id);
```

## Lấy trạng thái và kết quả của job

`GET /api/v1/jobs/{job_id}`

Trả về trạng thái của job và các kết quả tiếp theo đã sẵn sàng. Mỗi mục trong `results` hoặc là một phản hồi `/read` đầy đủ, hoặc là `{url, error}` cho một trang không đọc được. Khi có `next_cursor`, hãy gọi lại với `cursor` được đặt bằng giá trị đó; khi không có, chưa có thêm kết quả nào sẵn sàng. Các giá trị chỉ mang tính minh họa.

Xác thực: khóa API trong header `Authorization: Bearer API_KEY`.

**Tham số**

| Tên | Vị trí | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- | --- |
| `job_id` | path | string | Có | ID được trả về bởi `/batch` hoặc `/crawl`. |
| `cursor` | query | string | Không | Giá trị `next_cursor` từ lần gọi trước, để lấy các kết quả tiếp theo. |

**Response** `200`

```json
{
  "job_id": "job_8b27f41c0e",
  "status": "running",
  "results": [
    {
      "url": "https://docs.example.com/docs/install",
      "final_url": "https://docs.example.com/docs/install",
      "status_code": 200,
      "rendered": false,
      "credits_used": 1,
      "title": "Install",
      "markdown": "# Install\n\nRun the installer...",
      "word_count": 418
    },
    {
      "url": "https://docs.example.com/docs/private",
      "error": {
        "code": "blocked_by_robots",
        "message": "robots.txt disallows this page for WebReaderAPI."
      }
    }
  ],
  "next_cursor": "c_000002",
  "credits_used": 1
}
```

*curl*

```bash
curl -sG "https://webreaderapi.com/api/v1/jobs/job_8b27f41c0e" \
  -H "Authorization: Bearer $WEBREADER_API_KEY" \
  -d cursor=c_000002
```

*Python*

```python
import os
import requests

resp = requests.get(
    "https://webreaderapi.com/api/v1/jobs/job_8b27f41c0e",
    headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
    params={"cursor": "c_000002"},
    timeout=30,
)
job = resp.json()
print(job["status"], len(job["results"]), job.get("next_cursor"))
```

*Node.js*

```javascript
const res = await fetch('https://webreaderapi.com/api/v1/jobs/job_8b27f41c0e?cursor=c_000002', {
  headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}` },
});
const job = await res.json();
console.log(job.status, job.results.length, job.next_cursor);
```

## Làm sao để chờ một job hoàn tất?

Poll `/jobs/{job_id}` và giữ cursor. Xử lý các kết quả nhận được, rồi gọi lại với `cursor` được đặt bằng `next_cursor`. Khi không có `next_cursor` và `status` là `done`, bạn đã có tất cả; khi không có nó và job vẫn `running`, hãy chờ và hỏi lại với cùng cursor.

Bắt đầu poll mỗi 2 giây và tăng gấp đôi thời gian chờ lên tối đa 30 giây khi không có gì mới. Một crawl tuân thủ Crawl-delay của site có thể mất khá lâu, nên chờ lâu là bình thường và không tốn gì.

*Python: poll một job và lưu mọi trang thành file .md*

```python
import os
import pathlib
import re
import time

import requests

JOBS = "https://webreaderapi.com/api/v1/jobs"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}


def save_job(job_id, folder="pages"):
    out = pathlib.Path(folder)
    out.mkdir(exist_ok=True)
    cursor, wait, saved = None, 2, 0
    while True:
        params = {"cursor": cursor} if cursor else {}
        job = requests.get(f"{JOBS}/{job_id}", headers=HEADERS, params=params, timeout=30).json()
        for item in job["results"]:
            if "error" in item:
                print("skipped", item["url"], item["error"]["code"])
                continue
            name = re.sub(r"[^a-z0-9]+", "-", item["final_url"].lower()).strip("-")[:120]
            (out / f"{name}.md").write_text(item["markdown"], encoding="utf-8")
            saved += 1
        if job.get("next_cursor"):
            cursor, wait = job["next_cursor"], 2
            continue
        if job["status"] == "done":
            return saved
        time.sleep(wait)
        wait = min(wait * 2, 30)


if __name__ == "__main__":
    print(save_job("job_8b27f41c0e"), "pages saved")

```

## Một crawl đọc những trang nào?

Một crawl bắt đầu từ URL của bạn và lan ra ngoài từng bước liên kết một, theo chiều rộng, tối đa `max_depth`. Nó cũng đọc sitemap của site, từ địa chỉ được nêu trong robots.txt hoặc từ `/sitemap.xml`, để tìm những trang ít được liên kết tới.

- **Chỉ cùng domain.** Liên kết tới domain khác được liệt kê trong kết quả khi bạn yêu cầu `include_links`, nhưng không được đọc.
- **Bộ lọc đường dẫn.** `include_paths` và `exclude_paths` nhận các mẫu đơn giản như `/blog/*`.
- **Một bản sao cho mỗi trang.** Khi một site khai báo canonical URL, một trang được truy cập qua nhiều địa chỉ chỉ được đọc một lần.
- **Điểm dừng cứng.** Job kết thúc khi đã đọc `limit` trang.

## Một crawl tôn trọng site như thế nào?

Mỗi request đều mang User-Agent `WebReaderAPI/1.0 (+https://webreaderapi.com/bot/)`, và trình thu thập dữ liệu tuân theo quy tắc robots.txt cho `WebReaderAPI`, hoặc cho tất cả robot khi không có robot nào được nêu tên. Các trang bị disallow sẽ bị bỏ qua và xuất hiện trong `results` với lỗi `blocked_by_robots`; chúng không tốn gì.

Khi robots.txt đặt `Crawl-delay`, trình thu thập dữ liệu chờ chừng đó giữa hai request tới site. Nếu một website khai báo Content-Signal: ai-input=no trong robots.txt, chúng tôi không đọc các trang của trang đó cho bạn. Nếu một website từ chối yêu cầu của chúng tôi, chúng tôi trả lại câu trả lời của trang đó: chúng tôi không bao giờ giải CAPTCHA, đăng nhập vào tài khoản, hay tìm cách vượt qua chặn hoặc paywall.

## Một crawl tốn bao nhiêu?

Mỗi trang được đọc tốn bằng một lần gọi `/read`: 1 credit, 2 nếu trang cần trình duyệt, cộng thêm 10 với `country`. Trang bị bỏ qua và thất bại miễn phí. `credits_used` của job cộng dồn những gì đã được tính phí tới thời điểm hiện tại.

Một site tài liệu 500 trang đọc không cần trình duyệt dùng khoảng 500 credit, mà pack 10.000 trang (3,75 US$) đủ trả nhiều lần. Vì job không thể hủy trong v1, `limit` là mức trần chi tiêu của bạn: hãy đặt nó bằng số trang nhiều nhất bạn sẵn sàng trả tiền.

## Batch và crawl: các câu hỏi thường gặp

### Tôi nên dùng /batch hay nhiều lần gọi /read?

Dùng `/batch` khi bạn đã có sẵn các URL và không cần từng câu trả lời ngay lập tức. Nó giúp bạn khỏi phải quản lý concurrency và retry. Dùng `/read` khi một agent cần một trang ngay bây giờ.

### Kết quả của job được giữ bao lâu?

Hãy thu thập chúng ngay khi sẵn sàng. Kết quả chờ cho đến khi bạn fetch chúng; một khi đã giao, không giữ bản sao nào của nội dung trang, nên hãy lưu những gì bạn cần ở phía mình.

### Một crawl có thể theo liên kết tới các trang web khác không?

Không. Một crawl chỉ đọc một domain. Các domain khác xuất hiện dưới dạng liên kết khi bạn yêu cầu, và bạn có thể gửi các URL đó tới `/batch` nếu muốn chúng được đọc.

### Tôi có thể dừng một crawl đã bắt đầu không?

Không trong v1. Job dừng ở `limit` trang, nên hãy chọn giới hạn phù hợp với ngân sách của bạn. Các trang đã đọc vẫn bị tính phí; các trang chưa bao giờ đọc không tốn gì.

---

WebReaderAPI: WebReaderAPI là web reader API cho AI agent, chuyển mọi trang web công khai thành Markdown sạch, thanh toán bằng crypto không cần tải lên giấy tờ. Credit trả trước từ 0,07 US$/1K trang không bao giờ hết hạn, và robots.txt được tôn trọng. Không cần KYC — không ID, không selfie, không giấy tờ. Thanh toán bằng crypto.
