# API de batch y rastreo: lee muchas URL o un sitio web entero

> Referencia de la API: lee hasta 100 URL por trabajo o un sitio entero de hasta 10.000 páginas, respetando robots.txt. Paga con criptomonedas.

- URL: https://webreaderapi.com/es/docs/crawl/
- Última actualización: 2026-10-22
- Editor: WebReaderAPI (https://webreaderapi.com)

Dos endpoints inician trabajos en segundo plano: `/batch` para una lista de hasta 100 URL y `/crawl` para un sitio web. Ambos devuelven un ID de trabajo; recoges las páginas desde `/jobs/{job_id}`, una página de resultados a la vez.

**Respuesta rápida — ¿Cómo rastreo un sitio web a Markdown con WebReaderAPI?**

Envía la URL de inicio a `POST https://webreaderapi.com/api/v1/crawl` con una página `limit`. **WebReaderAPI** lee el sitio en segundo plano, solo el mismo dominio, siguiendo su sitemap, robots.txt y Crawl-delay, hasta 10.000 páginas por trabajo. Recoge las páginas Markdown del trabajo, una página de resultados a la vez. Cada página leída cuesta 1 crédito (2 con navegador), desde 0,07 US$/1K páginas a octubre de 2026. Sin KYC; paga con criptomonedas.

## Trabajos de un vistazo

| Atributo | Valor |
| --- | --- |
| Batch | Hasta 100 URLs por trabajo por lotes: `POST https://webreaderapi.com/api/v1/batch` |
| Rastreo (crawl) | Rastrea un sitio web por trabajo, hasta 10.000 páginas, siguiendo su sitemap, su robots.txt y su Crawl-delay: `POST https://webreaderapi.com/api/v1/crawl` |
| Resultados | `GET https://webreaderapi.com/api/v1/jobs/{job_id}`, luego `?cursor=…` para la siguiente página de resultados |
| Facturación | Cada página leída se factura como una llamada a `/read`; las páginas omitidas por robots.txt y las páginas fallidas no cuestan nada |
| Alcance de un rastreo | Un dominio; los enlaces a otros dominios se listan pero no se leen |
| Cancelar un trabajo | No disponible en v1: fija `limit` en el número máximo de páginas que estás dispuesto a pagar |

## Iniciar un trabajo de batch

`POST /api/v1/batch`

Lee hasta 100 URL en segundo plano con las mismas opciones. Las opciones usan los nombres y valores de [/read](https://webreaderapi.com/es/docs/read/). La respuesta es el ID del trabajo; los valores son ilustrativos.

Autenticación: clave API en el encabezado `Authorization: Bearer API_KEY`.

**Cuerpo de la solicitud**

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `urls` | array of strings | Sí | De 1 a 100 URLs http o https. Los duplicados se leen una vez. |
| `options` | object | No | Cualquier parámetro `/read` excepto `url`: `format`, `main_content`, `render`, `include_links`, `include_images`, `country`, `max_chars`, `timeout`. |

*Ejemplo: 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
  }
}
```

**Respuesta** `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);
```

## Iniciar un trabajo de rastreo (crawl)

`POST /api/v1/crawl`

Lee un sitio web desde una URL de inicio, en anchura (breadth first). El rastreador se mantiene en el dominio de la URL de inicio, usa el sitemap indicado en robots.txt o encontrado en `/sitemap.xml`, y respeta robots.txt y Crawl-delay. Los valores son ilustrativos.

Autenticación: clave API en el encabezado `Authorization: Bearer API_KEY`.

**Cuerpo de la solicitud**

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `url` | string | Sí | La página de inicio, como la portada de un sitio de documentación. |
| `limit` | integer | No | Número máximo de páginas a leer, hasta 10.000. El trabajo se detiene al alcanzar este número. |
| `max_depth` | integer | No | Número máximo de pasos de enlace desde la URL de inicio. |
| `include_paths` | array of strings | No | Patrones de ruta simples que conservar, como `/docs/*`. |
| `exclude_paths` | array of strings | No | Patrones de ruta simples que omitir, como `/blog/tag/*`. |
| `options` | object | No | Cualquier parámetro `/read` excepto `url`, aplicado a cada página. |

*Ejemplo: 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"
  }
}
```

**Respuesta** `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);
```

## Consultar el estado y los resultados de un trabajo

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

Devuelve el estado del trabajo y los siguientes resultados que estén listos. Cada elemento de `results` es una respuesta `/read` completa o `{url, error}` para una página que no se pudo leer. Cuando `next_cursor` está presente, vuelve a llamar con `cursor` ajustado a él; cuando no está, todavía no hay más resultados listos. Los valores son ilustrativos.

Autenticación: clave API en el encabezado `Authorization: Bearer API_KEY`.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `job_id` | path | string | Sí | El ID devuelto por `/batch` o `/crawl`. |
| `cursor` | query | string | No | El valor de `next_cursor` de la llamada anterior, para obtener los resultados siguientes. |

**Respuesta** `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);
```

## ¿Cómo espero a que termine un trabajo?

Haz polling a `/jobs/{job_id}` y conserva el cursor. Procesa los resultados que recibas y vuelve a llamar con `cursor` ajustado a `next_cursor`. Cuando `next_cursor` está ausente y `status` es `done`, ya tienes todo; cuando está ausente y el trabajo sigue en `running`, espera y vuelve a preguntar con el mismo cursor.

Empieza a hacer polling cada 2 segundos y duplica la espera hasta 30 segundos mientras no llegue nada nuevo. Un rastreo que respeta el Crawl-delay de un sitio puede tardar, así que una espera larga es normal y no cuesta nada.

*Python: hacer polling de un trabajo y guardar cada página como un archivo .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")

```

## ¿Qué páginas lee un rastreo?

Un rastreo parte de tu URL y avanza hacia fuera un paso de enlace a la vez, en anchura, hasta `max_depth`. También lee el sitemap del sitio, desde la dirección indicada en robots.txt o desde `/sitemap.xml`, para encontrar páginas a las que apuntan pocos enlaces.

- **Solo el mismo dominio.** Los enlaces a otros dominios se listan en los resultados cuando pides `include_links`, pero no se leen.
- **Filtros de ruta.** `include_paths` y `exclude_paths` aceptan patrones simples como `/blog/*`.
- **Una copia por página.** Cuando un sitio declara una URL canónica, una página alcanzada por varias direcciones se lee una vez.
- **Un tope estricto.** El trabajo termina cuando ha leído `limit` páginas.

## ¿Cómo respeta un rastreo al sitio?

Cada solicitud lleva el User-Agent `WebReaderAPI/1.0 (+https://webreaderapi.com/bot/)`, y el rastreador sigue las reglas de robots.txt para `WebReaderAPI`, o para todos los robots cuando ninguno lo nombra. Las páginas no permitidas se omiten y aparecen en `results` con el error `blocked_by_robots`; no cuestan nada.

Cuando robots.txt define un `Crawl-delay`, el rastreador espera ese tiempo entre dos solicitudes al sitio. Si un sitio declara Content-Signal: ai-input=no en su robots.txt, no leemos sus páginas para ti. Si un sitio rechaza nuestra solicitud, devolvemos su respuesta: nunca resolvemos CAPTCHAs, iniciamos sesión en cuentas ni intentamos saltar un bloqueo o un paywall.

## ¿Cuánto cuesta un rastreo?

Cada página leída cuesta lo mismo que una llamada `/read`: 1 crédito, 2 si la página necesitó un navegador, más 10 con `country`. Las páginas omitidas y las fallidas son gratis. El campo `credits_used` del trabajo suma lo facturado hasta el momento.

Un sitio de documentación de 500 páginas leído sin navegador usa unos 500 créditos, que el pack 10.000 páginas (3,75 US$) cubre muchas veces. Como un trabajo no se puede cancelar en v1, `limit` es tu límite de gasto: ajústalo al máximo de páginas que estás dispuesto a pagar.

## Batch y crawl: preguntas frecuentes

### ¿Debo usar /batch o muchas llamadas a /read?

Usa `/batch` cuando ya tengas las URLs y no necesites cada respuesta de inmediato. Te ahorra gestionar la concurrencia y los reintentos. Usa `/read` cuando un agente necesite una página ahora.

### ¿Cuánto tiempo se conservan los resultados de un trabajo?

Recógelos en cuanto estén listos. Los resultados esperan hasta que los descargues; una vez entregados, no se conserva ninguna copia del contenido de la página, así que guarda lo que necesites de tu lado.

### ¿Puede un rastreo seguir enlaces a otros sitios web?

No. Un rastreo lee un solo dominio. Los otros dominios aparecen como enlaces cuando los pides, y puedes enviar esas URLs a `/batch` si quieres que se lean.

### ¿Puedo detener un rastreo que ya ha empezado?

No en v1. El trabajo se detiene a las `limit` páginas, así que elige un límite que se ajuste a tu presupuesto. Las páginas ya leídas siguen facturándose; las páginas que nunca se leyeron no cuestan nada.

---

WebReaderAPI: WebReaderAPI es una API de lectura web para agentes de IA que convierte cualquier página web pública en Markdown limpio, pagando con criptomonedas y sin subir ningún documento de identidad. Los créditos prepago desde 0,07 US$/1K páginas nunca caducan y se respeta robots.txt. Sin KYC: ni documento de identidad, ni selfie, ni documentos. Paga con criptomonedas.
