Se ejecuta enWrit CloudDesktopAutoalojado
En esta página
Una página, o todas.
La superficie de crawl son tres llamadas que crecen con el trabajo: una página, un mapa de las URL, o todas las páginas dentro de un alcance que tú fijas. Guarda el alcance y se vuelve invocable — POST /api/crawl/definitions/{ref}/run responde desde la última ejecución si está lo bastante fresca, y vuelve a crawlear si no.
superficies ▸ tres tamaños
Tres llamadas, tres tamaños de trabajo.
Usa la más pequeña que responda tu pregunta. Una sola página es una página medida; un mapa es una lista de URL; un crawl recorre el alcance y llena un dataset que puedes leer, buscar y exportar.
| Llamada | Qué hace | Coste |
|---|---|---|
POST /api/crawl/scrape | Una página, devuelta en markdown con recuento de caracteres y de tokens. | 1 página |
POST /api/crawl/map | Las URL que expone un sitio, sin cargar cada una entera. | Medido |
POST /api/crawl | Inicia un crawl sobre el alcance. Devuelve un job que consultas. | Por página |
POST /v1/keyless/crawl | Unas pocas páginas del mismo dominio, un nivel, SIN cuenta. Devuelve las páginas en línea, no un job. | Gratis, con tope diario |
POST /api/crawl/preview | El alcance que un crawl USARÍA — include/exclude/profundidad efectivos, más una muestra de URL conservadas frente a descartadas. | Nada |
Una página, dos niveles
Con una clave wt_ la llamada se mide contra tu plan. Sin clave alguna, el nivel keyless devuelve la misma forma para páginas públicas — más abajo. El nivel sin clave también rastrea: POST /v1/keyless/crawl carga hasta 5 páginas del mismo dominio, un nivel de profundidad, y las devuelve en línea — sin flota, sin persona, sin salida residencial. Cada página gasta la misma asignación diaria que una llamada a <code>/v1/keyless/scrape</code>: el tope diario, no el de la petición, es el límite real, y la respuesta indica ambos.
cloud.ts
const cloud = new CloudApi({ apiKey: process.env.WRIT_API_KEY }); // wt_…
const page = await cloud.scrape("https://example.com");
const site = await cloud.map("https://example.com", { search: "pricing", limit: 20 });
console.log(cloud.tier); // "metered" | "keyless" cloud.py
cloud = Cloud(api_key=os.environ["WRIT_API_KEY"]) # wt_… — no daemon needed
page = cloud.scrape("https://example.com")
site = cloud.map("https://example.com", search="pricing", limit=20)
print(cloud.tier) # "metered" | "keyless" metered.sh
# Metered — wt_ API key, billed from your credit pool
curl -X POST https://api.usewrit.app/api/crawl/scrape \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' keyless.sh
# Keyless — no account, no key: a stable device id is the only identity.
# 429 keyless_rate_limited when the allowance is spent.
curl -X POST https://api.usewrit.app/v1/keyless/scrape \
-H "X-Writ-Client-Id: $WRIT_CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
curl https://api.usewrit.app/v1/keyless/quota -H "X-Writ-Client-Id: $WRIT_CLIENT_ID" Qué devuelve una página
La llamada de una página devuelve un cuerpo plano, no un handle de job:
verb | Qué operación respondió. |
url · title | La página leída y su título. |
format | Siempre "markdown" en esta llamada. |
markdown | El cuerpo de la página, limpio. |
counts | chars, raw_tokens_est y clean_tokens_est — lo que costaría leerla a un modelo. |
tier | "metered" cuando una clave wt_ autenticó la llamada. |
Dos fallos que conviene manejar: 422 scrape_unreachable cuando la página no se puede cargar, y 402 insufficient_credits cuando la página excede tu plan y el wallet no la cubre.
alcance ▸ qué se carga
Di qué crawlear, y hasta dónde.
Solo url es obligatorio. Todo lo demás estrecha el alcance, cambia cómo se lee una página, o pone tope al trabajo. Los filtros de ruta son expresiones regulares aplicadas a la ruta.
| Campo | Qué hace |
|---|---|
url | Obligatorio. La semilla desde la que arranca el crawl. |
name | Una etiqueta para el job, para que la lista se lea como tu trabajo y no como URL. |
executor | regular | ai — regular por defecto. El executor ai razona sobre cada página y pesa 5×. |
extract_mode | markdown | schema — markdown por defecto. |
extract_schema | La forma de los campos a extraer cuando extract_mode es schema. |
extract_prompt | Instrucción en lenguaje llano para el executor ai. |
render_mode | auto | http | browser. Un render de navegador pesa 2×. |
ocr_mode | auto | off | force — ver documentos más abajo. Una página con OCR pesa 2×. |
persona_id | Crawlear con sesión iniciada, usando una identidad de acceso guardada tuya. |
use_residential | Ruta de red premium. Disponible en planes premium. |
intent | Un objetivo en lenguaje llano. Deriva el alcance y ordena qué URL vale la pena visitar primero. |
seed_urls[] | Puntos de partida adicionales, además de url. |
relevance_threshold | 0–1. Cuánto debe encajar una página con el intent para conservarse. |
include_paths[] · exclude_paths[] | Regex de ruta. Include estrecha, exclude resta. |
max_depth | 0–20 enlaces desde la semilla. |
page_budget | 1–50000, 1000 por defecto. El tope duro de este crawl. |
max_concurrent_shards | 1–64. Cuán ancho corre el crawl. |
shard_size | 1–200, 25 por defecto. Páginas por unidad de trabajo. |
delay_ms | 0–60000, 250 por defecto. Pausa entre peticiones. |
respect_robots | true por defecto. |
same_domain · allow_subdomains | Ambos true por defecto. |
content_spec | { preset, include_comments, exclude_selectors, include_selectors, keep } — qué parte de cada página se conserva. |
Dos formas de apuntar. Dale include_paths y max_depth y habrás descrito la forma con exactitud. Dale intent en su lugar y habrás descrito el objetivo — el crawl deriva de ahí un alcance y ordena la frontera por cuánto encaja cada URL, con relevance_threshold como corte.
POST /api/crawl/preview devuelve el alcance que un crawl usaría de verdad — los patrones include y exclude efectivos, la profundidad, y una muestra de URL que conservaría junto a otras que descartaría. No carga nada y no cuesta nada. Lánzalo antes de un presupuesto grande.
Iniciar un crawl
En el agente local el mismo job arranca en 127.0.0.1:8131 con un token local, y el dataset que llena se relee por la superficie de datos.
crawl.ts
const job = await client.crawl.start({
url: "https://example.com",
max_depth: 3,
page_budget: 500,
});
const status = await client.crawl.get(job.id);
const table = await client.data.workflowData(job.data_workflow_id); crawl.py
job = client.crawl.start("https://example.com", max_depth=3, page_budget=500)
job = client.crawl.get(job["id"])
table = client.data.workflow_data(job["data_workflow_id"]) crawl.go
job, _ := client.Crawl.Start(ctx, writ.CrawlStartParams{URL: "https://example.com"})
st, _ := client.Crawl.Get(ctx, job.ID) crawl.rs
use writ_client::CrawlStartParams;
let job = agent.crawl().start(CrawlStartParams {
url: "https://example.com".into(),
..Default::default()
}).await?;
let job = agent.crawl().get(job.id).await?; progreso ▸ estado
Míralo trabajar. Párala cuando quieras.
Un crawl es un job, no una petición: sobrevive con normalidad a cualquier timeout HTTP sensato, así que recibes un handle y lo consultas. GET /v1/crawl los lista (limit 1–500, 50 por defecto) bajo una clave crawls; GET /v1/crawl/{id} lee uno, o 404 si no es tuyo.
| status | Qué significa |
|---|---|
queued | Aceptado, esperando para empezar. |
mapping | Averiguando qué URL están dentro del alcance. |
crawling | Cargando páginas. |
stopping | Cancelación reconocida, terminando lo que está en vuelo. |
completed | Terminal. Todo el alcance se visitó o lo cortó el presupuesto. |
failed | Terminal. Lee error para saber por qué. |
cancelled | Terminal. Pediste que parara. |
Los contadores de un crawl
Cada lectura de un crawl lleva los mismos campos, así que un solo consultador sirve para todos los sitios de ejecución:
| Campo | Qué contiene |
|---|---|
id · name · seed_url | Identidad y punto de partida. |
include_paths · exclude_paths · max_depth | El alcance, tal como quedó resuelto. |
same_domain · allow_subdomains · respect_robots | Las reglas de frontera vigentes. |
extract_mode · extract_schema | Qué se extrae de cada página. |
persona_id | La identidad de acceso usada, si la hubo. |
delay_ms · max_concurrent · page_budget | El ritmo y el techo. |
workflow_id · data_workflow_id | Dónde aterrizan las filas recogidas. |
pages_discovered · pages_done · pages_failed · pages_skipped | Los cuatro contadores que vale la pena graficar. |
workers_active · current_depth | Cuán ancho y cuán profundo va ahora mismo. |
status · error · cancel_requested · is_terminal | Dónde está, y si volverá a moverse. |
created_at · updated_at · started_at · completed_at | La cronología. |
Una rareza que conviene saber antes de escribir el cliente: los campos booleanos de un crawl vuelven como los enteros 0 y 1, no como true y false de JSON. Compáralos como números, o conviértelos a la entrada.
POST /v1/crawl/{id}/cancel responde siempre 200, en el estado que estuviera el job, y devuelve el crawl actualizado más cancel_requested_now — verdadero cuando fue tu llamada la que lo cambió. Cancelar un crawl ya terminado no es un error, así que reintentar es seguro.
guardado ▸ invocable
Guarda un crawl y llámalo como una API.
Una definición es una configuración de crawl con nombre y slug. Convierte un job puntual en algo que una clave puede llamar: /v1/crawl/definitions en el agente local, /api/crawl/definitions en Writ Cloud. Ambos aceptan un slug o un id como {ref}.
| Campo | Qué hace |
|---|---|
name | Hasta 200 caracteres. |
slug | Hasta 120 caracteres. El nombre que usan tus llamadas. |
description | Texto libre, para quien lea la lista después. |
default_max_age_seconds | La ventana de frescura a aplicar cuando una llamada no dice nada. |
config | La configuración de crawl a ejecutar. Los mismos campos que al iniciar un crawl. |
from_crawl_id | O bien: copiar la configuración de un crawl que ya ejecutaste. |
Envía exactamente uno de config o from_crawl_id. No enviar ninguno responde 400 — la definición no tendría nada que ejecutar.
Ejecutarla
El cuerpo de ejecución son cuatro campos, todos opcionales:
max_age | 0 segundos o más. La ventana de frescura de esta llamada. También se acepta como ?max_age= o un Cache-Control max-age. |
wait | false por defecto. En true, la llamada HTTP bloquea hasta que el crawl se resuelve. |
timeout | 5–300 segundos, 120 por defecto. Solo tiene sentido con wait. |
limit | 1–500, 50 por defecto. Cuántas filas recogidas vuelven en línea. |
El contrato de frescura
Esta es la parte contra la que programar. El código de estado dice qué pasó, y ninguna respuesta es un callejón sin salida:
| Respuesta | Qué pasó |
|---|---|
200 · cached: true | La última ejecución cayó dentro de la ventana. Sus datos vuelven en línea. No se crawleó nada y no se midió nada. |
202 | Fallo de ventana. Arrancó un crawl fresco; el cuerpo lleva el crawl y su status_url. Consúltalo. |
504 | wait: true rebasó su timeout. El crawl sigue corriendo y el cuerpo aún lleva crawl_id y status_url — recógelo, no reintentes. |
Cache-Control: no-cache · max_age: 0 | Vuelve a crawlear siempre, diga lo que diga el valor por defecto de la definición. |
Cada respuesta lleva un objeto _cache — hit, age_seconds y source_crawl_id — para que un cliente pueda registrar por qué recibió lo que recibió. Y GET /v1/crawl/definitions/{ref}/data es una lectura pura de la última ejecución completada, a cualquier edad: nunca crawlea y nunca factura.
Guardar, llamar, leer
Tres llamadas: crear la definición, ejecutarla con una ventana, leer lo que ya contiene.
save-and-run.sh
# 1. Save the crawl — name + slug + the config it should always run.
curl -X POST https://api.usewrit.app/api/crawl/definitions \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Docs index",
"slug": "docs-index",
"default_max_age_seconds": 86400,
"config": {
"url": "https://example.com/docs",
"include_paths": ["^/docs/"],
"max_depth": 3,
"page_budget": 500
}
}'
# 2. Call it. Fresh enough? You get the data. Stale? It re-crawls.
curl -X POST https://api.usewrit.app/api/crawl/definitions/docs-index/run \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"max_age": 86400, "wait": true, "timeout": 120, "limit": 50}' 200 — the window held
{
"cached": true,
"_cache": { "hit": true, "age_seconds": 3512, "source_crawl_id": 8811 },
"definition": { "name": "Docs index", "slug": "docs-index" },
"crawl": { "id": 8811, "status": "completed", "pages_done": 412 },
"status_url": "/api/crawl/8811",
"data": [ { "url": "https://example.com/docs/intro", "title": "Intro" } ]
}
// A miss answers 202 with the new crawl and its status_url instead.
// wait:true that overruns answers 504 — still carrying crawl_id and
// status_url, so the pages already paid for stay collectable. read-only.sh
# What the last completed run collected — any age, never crawls, never bills.
curl "https://api.usewrit.app/api/crawl/definitions/docs-index/data?limit=50" \
-H "Authorization: Bearer $WRIT_API_KEY"
# Force a fresh pass, whatever the definition's default window says.
curl -X POST https://api.usewrit.app/api/crawl/definitions/docs-index/run \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Cache-Control: no-cache" documentos ▸ ocr
PDF, hojas de cálculo y páginas escaneadas.
Un sitio rara vez es solo HTML. Los PDF, los archivos de Word, Excel y PowerPoint, las imágenes y las páginas escaneadas se leen dentro del crawl, con OCR donde el texto son solo píxeles. Lo gobierna un único mando:
| ocr_mode | Qué hace |
|---|---|
auto | Se usa OCR cuando una página o documento no tiene capa de texto legible. |
off | Nunca ejecuta OCR. Los documentos con capa de texto se siguen leyendo. |
force | OCR en cada página, incluso cuando existe capa de texto. |
Dónde aplica: crawls que corren en la flota de Writ Cloud, y crawls autoalojados. Un crawl cloud que enrutas a tu propia máquina vinculada no hace extracción de documentos — esa vía lee páginas, no documentos. Elige el sitio de ejecución en consecuencia. Las páginas con OCR se miden 2×.
precio ▸ por página
Una página cuesta $0,0005. Algunas pesan más.
El crawl se paga por uso, por página. Dentro de las páginas de crawl mensuales de tu plan no cuesta nada extra; por encima se cobra del wallet a la misma tarifa, y un wallet que no cubre la página responde 402 insufficient_credits. Los despliegues autoalojados rastrean con tus propios agentes y no llevan ningún cargo por página por nuestra parte.
| Cómo se leyó la página | Peso |
|---|---|
| Página HTTP simple, o documento | 1× |
| Render de navegador | 2× |
| Página con OCR | 2× |
| executor: "ai" | 5× |
Qué incluye cada plan
Tres números distintos por plan. Léelos como tres, no como uno — responden a tres preguntas diferentes.
| Plan | Páginas de crawl al mes | Páginas en un crawl | Crawls a la vez |
|---|---|---|---|
| Free | 1.000 | 1.000 | 1 |
| Starter | 15.000 | 10.000 | 2 |
| Pro | 75.000 | 25.000 | 3 |
| Growth | 400.000 | 50.000 | 6 |
| Scale | 1.000.000 | 50.000 | 12 |
| Enterprise | 2.000.000 | 50.000 | 24 |
Son números distintos. «Páginas en un crawl» es el techo de un solo job; «páginas de crawl al mes» es lo que tu plan incluye entre todos los jobs del periodo. En Pro son 25.000 y 75.000 — el mismo plan hace tres crawls a tamaño completo al mes antes de que se facture nada.
También fallan de forma distinta. Un page_budget mayor que tu tope por crawl se recorta al tope y el crawl se ejecuta — no se te rechaza por pedirlo. Las dos barreras que sí pueden negarse son el límite de concurrencia (un crawl de más mientras otros corren) y la asignación mensual una vez que el wallet no cubre el exceso.
Leer el contador
GET /api/crawl/meta/usage devuelve tu posición actual, para que un cliente decida antes de gastar:
pages_included_per_month | La asignación del plan para el periodo. |
pages_used_this_period · pages_remaining | Dónde estás dentro de ella. |
per_job_page_cap | El recorte que se aplica a un solo crawl. |
max_concurrent_crawls | Cuántos pueden correr a la vez. |
overage_price_micros_per_page | Lo que cuesta una página por encima de la asignación. |
browser_page_units · ocr_page_units | Los multiplicadores de peso, para que tu estimación cuadre con la factura. |
usage.sh
curl https://api.usewrit.app/api/crawl/meta/usage \
-H "Authorization: Bearer $WRIT_API_KEY" 200
{
"pages_included_per_month": 75000,
"pages_used_this_period": 12480,
"pages_remaining": 62520,
"per_job_page_cap": 25000,
"max_concurrent_crawls": 3,
"overage_price_micros_per_page": 500,
"browser_page_units": 2,
"ocr_page_units": 2
}
// -1 anywhere in this body means unlimited. keyless ▸ sin cuenta
¿Sin cuenta? Una página cada vez.
El nivel keyless lee páginas públicas sin cuenta y sin clave. Un dispositivo se identifica por una cabecera de id de cliente, y la asignación es pequeña a propósito: existe para que un SDK o la app de escritorio funcione antes de registrarte.
| Llamada | Qué hace |
|---|---|
POST /v1/keyless/scrape | Markdown completo de una página pública. Cuesta 1 petición y 1 página. |
POST /v1/keyless/map | Hasta 200 URL de un sitio. Cuesta 1 petición, 0 páginas. |
GET /v1/keyless/quota | Lo que queda. No gasta nada. |
POST /v1/keyless/crawl | Siempre 402 api_key_required — crawlear un sitio entero exige cuenta. |
Los topes
- 10 peticiones y 20 páginas al día, por dispositivo.
- 30 peticiones al día y 10 por minuto, por dirección IP.
- Un mapa devuelve como mucho 200 URL.
- Al pasarse de cualquiera: 429 keyless_rate_limited.
Cada llamada keyless lleva X-Writ-Client-Id — un id de dispositivo estable. Los SDKs y la app de escritorio lo ponen por ti; una llamada sin él responde 400 client_id_required.
claves ▸ scopes
Tres scopes, desiguales a propósito.
Leer una página es algo más pequeño que crawlear un sitio, así que es un scope más pequeño. Una clave entregada a un socio para lecturas de una página no puede iniciar un crawl contra tu asignación.
| Scope | Qué concede |
|---|---|
crawl:execute | Iniciar y cancelar crawls; crear, actualizar y ejecutar crawls guardados. |
crawl:read | Listar y leer crawls, crawls guardados y sus datos recogidos. |
scrape:execute | Lecturas de una página, mapa y preview. Aparte, y menor. |
mcp ▸ herramientas
El mismo crawl, como herramientas MCP.
El servidor MCP de escritorio expone la superficie de crawl como herramientas, para que un cliente de IA lance y relance un crawl sin que tú escribas nada de HTTP:
| Herramienta | Qué hace |
|---|---|
writ_crawl_site | url, extract (markdown | schema), extract_schema, max_pages, max_depth, include[], exclude[], same_domain, allow_subdomains, content{}, persona, save_as, max_age. |
writ_crawl_status | Cómo va un crawl en curso. |
writ_saved_crawls | Los crawls guardados que puedes llamar por su nombre. |
writ_run_saved_crawl | crawl, max_age, limit — el contrato de frescura, como herramienta. |
writ_saved_crawl_data | Lo que un crawl guardado ya recogió. Nunca crawlea. |
writ_scrape · writ_map | Una página, o la lista de URL. |
Dos comportamientos que conviene saber. Reutilizar un nombre save_as actualiza ese crawl guardado en vez de crear un segundo — así un cliente de IA que va afinando su crawl te deja una definición, no doce. Y max_age solo significa algo junto a save_as: sin crawl guardado no hay ejecución previa que reutilizar.
crawl tools
// Crawl a site and save it under a callable name in one turn.
writ_crawl_site {
"url": "https://example.com/docs",
"extract": "markdown",
"max_pages": 500,
"max_depth": 3,
"include": ["^/docs/"],
"exclude": ["^/docs/legacy/"],
"same_domain": true,
"allow_subdomains": false,
"content": { "preset": "article", "exclude_selectors": ["nav", "footer"] },
"save_as": "docs-index"
}
// Re-using a save_as name UPDATES that saved crawl — it does not duplicate it.
// max_age only matters together with save_as.
writ_run_saved_crawl { "crawl": "docs-index", "max_age": 86400, "limit": 50 }
writ_saved_crawl_data { "crawl": "docs-index" }
writ_saved_crawls {}
writ_crawl_status { "crawl_id": 8811 }
writ_map { "url": "https://example.com" }
writ_scrape { "url": "https://example.com/pricing" } referencia ▸ siguiente
Adónde va el crawl después
Cada endpoint de crawl, campo a campo.
→ Datasets y archivosLeer, buscar y exportar lo que un crawl recogió.
→ MCPLas herramientas de crawl en cualquier cliente MCP.
→ ScribeDescribe el crawl con palabras y deja que arme el alcance.
→ Dónde se ejecutaFlota cloud, tu propia máquina, o autoalojado.
→ Facturación y usoCómo se miden juntos páginas, tiempo de ejecución y tokens de IA.
→faq
Preguntas de crawl, respondidas.
¿Qué diferencia hay entre el tope de páginas por crawl y la asignación mensual?
¿Cómo evito pagar por un crawl que ya lancé?
Mi llamada wait:true devolvió 504. ¿Perdí las páginas?
¿Por qué los booleanos de un crawl vuelven como 0 y 1?
¿Se incluyen los PDF y las páginas escaneadas?
¿Puedo crawlear un sitio entero sin cuenta?
fin ▸ lanzar uno
Previsualiza el alcance y lánzalo.
El preview no cuesta nada y muestra exactamente qué URL conservaría un crawl. Es la forma más barata de asegurarte antes de un presupuesto de páginas grande.