S’exécute surWrit CloudDesktopAuto-hébergé
Sur cette page
Une page, ou toutes les pages.
La surface de crawl tient en trois appels qui grandissent avec le travail : une page, une carte des URL, ou toutes les pages d’un périmètre que vous fixez. Enregistrez le périmètre et il devient appelable — POST /api/crawl/definitions/{ref}/run répond depuis la dernière exécution si elle est assez fraîche, et recrawle sinon.
surfaces ▸ trois tailles
Trois appels, trois tailles de travail.
Prenez le plus petit qui répond à votre question. Une page seule vaut une page décomptée ; une carte est une liste d’URL ; un crawl parcourt le périmètre et remplit un dataset que vous pouvez lire, fouiller et exporter.
| Appel | Ce qu’il fait | Coût |
|---|---|---|
POST /api/crawl/scrape | Une page, rendue en markdown avec un compte de caractères et de tokens. | 1 page |
POST /api/crawl/map | Les URL qu’un site expose, sans charger chacune en entier. | Décompté |
POST /api/crawl | Démarre un crawl sur le périmètre. Rend un job que vous interrogez. | Par page |
POST /v1/keyless/crawl | Quelques pages du même domaine, sur un niveau, SANS compte. Rend les pages en ligne, pas un job. | Gratuit, plafonné par jour |
POST /api/crawl/preview | Le périmètre qu’un crawl PRENDRAIT — include/exclude/profondeur effectifs, plus un échantillon d’URL gardées face à des URL écartées. | Rien |
Une page, deux paliers
Avec une clé wt_, l’appel est décompté de votre plan. Sans aucune clé, le palier keyless rend la même forme pour les pages publiques — voir plus bas. Le palier sans clé crawle aussi : POST /v1/keyless/crawl charge jusqu’à 5 pages du même domaine, sur un niveau, et les rend en ligne — sans flotte, sans persona, sans sortie résidentielle. Chaque page consomme la même allocation quotidienne qu’un appel à <code>/v1/keyless/scrape</code> : le plafond quotidien, pas celui par requête, est la vraie limite, et la réponse indique les deux.
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" Ce que rend une page
L’appel une-page rend un corps plat, pas un handle de job :
verb | L’opération qui a répondu. |
url · title | La page lue et son titre. |
format | Toujours "markdown" sur cet appel. |
markdown | Le corps de la page, nettoyé. |
counts | chars, raw_tokens_est et clean_tokens_est — ce que sa lecture coûterait à un modèle. |
tier | "metered" quand une clé wt_ a authentifié l’appel. |
Deux échecs à traiter : 422 scrape_unreachable quand la page ne peut pas être chargée, et 402 insufficient_credits quand la page dépasse votre plan et que le wallet ne peut pas la couvrir.
périmètre ▸ ce qui est chargé
Dites quoi crawler, et jusqu’où.
Seul url est obligatoire. Tout le reste resserre le périmètre, change la façon de lire une page, ou plafonne le travail. Les filtres de chemin sont des expressions régulières appliquées au chemin.
| Champ | Rôle |
|---|---|
url | Obligatoire. L’URL de départ du crawl. |
name | Un libellé pour le job, pour que la liste ressemble à votre travail et non à des URL. |
executor | regular | ai — regular par défaut. L’executor ai raisonne sur chaque page et pèse 5×. |
extract_mode | markdown | schema — markdown par défaut. |
extract_schema | La forme des champs à extraire quand extract_mode vaut schema. |
extract_prompt | Consigne en langage clair pour l’executor ai. |
render_mode | auto | http | browser. Un rendu navigateur pèse 2×. |
ocr_mode | auto | off | force — voir les documents plus bas. Une page OCR pèse 2×. |
persona_id | Crawler connecté, avec une identité de connexion enregistrée qui vous appartient. |
use_residential | Chemin réseau premium. Disponible sur les plans premium. |
intent | Un objectif en langage clair. Il dérive le périmètre et classe les URL à visiter en premier. |
seed_urls[] | Points de départ supplémentaires, en plus de url. |
relevance_threshold | 0–1. À quel point une page doit coller à l’intent pour être gardée. |
include_paths[] · exclude_paths[] | Regex de chemin. Include resserre, exclude retranche. |
max_depth | 0–20 liens depuis l’URL de départ. |
page_budget | 1–50000, 1000 par défaut. L’arrêt net de ce crawl. |
max_concurrent_shards | 1–64. La largeur du crawl. |
shard_size | 1–200, 25 par défaut. Pages par unité de travail. |
delay_ms | 0–60000, 250 par défaut. Pause entre les requêtes. |
respect_robots | true par défaut. |
same_domain · allow_subdomains | Les deux à true par défaut. |
content_spec | { preset, include_comments, exclude_selectors, include_selectors, keep } — la part de chaque page qui est gardée. |
Deux façons de viser. Donnez include_paths et max_depth et vous avez décrit la forme exactement. Donnez intent à la place et vous avez décrit le but — le crawl en dérive un périmètre et ordonne la frontière selon la proximité de chaque URL, avec relevance_threshold comme seuil.
POST /api/crawl/preview rend le périmètre qu’un crawl prendrait réellement — motifs include et exclude effectifs, profondeur, et un échantillon d’URL gardées à côté d’URL écartées. Il ne charge rien et ne coûte rien. Lancez-le avant un gros budget.
Démarrer un crawl
Sur l’agent local, le même job démarre sur 127.0.0.1:8131 avec un token local, et le dataset qu’il remplit se relit par la surface de données.
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?; avancement ▸ statut
Regardez-le travailler. Arrêtez-le quand vous voulez.
Un crawl est un job, pas une requête : il survit couramment à tout timeout HTTP raisonnable, donc vous recevez un handle et vous l’interrogez. GET /v1/crawl les liste (limit 1–500, 50 par défaut) sous une clé crawls ; GET /v1/crawl/{id} en lit un, ou 404 s’il n’est pas à vous.
| status | Ce que ça veut dire |
|---|---|
queued | Accepté, en attente de démarrage. |
mapping | Détermine quelles URL sont dans le périmètre. |
crawling | Charge les pages. |
stopping | Annulation prise en compte, termine ce qui est en vol. |
completed | Terminal. Tout le périmètre a été visité ou coupé par le budget. |
failed | Terminal. Lisez error pour la raison. |
cancelled | Terminal. Vous avez demandé l’arrêt. |
Les compteurs d’un crawl
Chaque lecture d’un crawl porte les mêmes champs, donc un seul poller couvre tous les lieux d’exécution :
| Champ | Contenu |
|---|---|
id · name · seed_url | Identité et point de départ. |
include_paths · exclude_paths · max_depth | Le périmètre, tel que résolu. |
same_domain · allow_subdomains · respect_robots | Les règles de frontière en vigueur. |
extract_mode · extract_schema | Ce qui est extrait de chaque page. |
persona_id | L’identité de connexion utilisée, le cas échéant. |
delay_ms · max_concurrent · page_budget | Le rythme et le plafond. |
workflow_id · data_workflow_id | Où atterrissent les lignes collectées. |
pages_discovered · pages_done · pages_failed · pages_skipped | Les quatre compteurs à tracer. |
workers_active · current_depth | Sa largeur et sa profondeur à l’instant présent. |
status · error · cancel_requested · is_terminal | Où il en est, et s’il bougera encore. |
created_at · updated_at · started_at · completed_at | La chronologie. |
Une bizarrerie à connaître avant d’écrire le client : les champs booléens d’un crawl reviennent en entiers 0 et 1, pas en true / false JSON. Testez-les comme des nombres, ou convertissez à l’entrée.
POST /v1/crawl/{id}/cancel répond toujours 200, quel que soit l’état du job, et rend le crawl rafraîchi plus cancel_requested_now — vrai quand c’est votre appel qui a basculé l’état. Annuler un crawl déjà fini n’est pas une erreur : réessayer est sans risque.
enregistré ▸ appelable
Enregistrez un crawl, puis appelez-le comme une API.
Une définition est une configuration de crawl avec un nom et un slug. Elle transforme un job ponctuel en quelque chose qu’une clé peut appeler : /v1/crawl/definitions sur l’agent local, /api/crawl/definitions sur Writ Cloud. Les deux acceptent un slug ou un id comme {ref}.
| Champ | Rôle |
|---|---|
name | Jusqu’à 200 caractères. |
slug | Jusqu’à 120 caractères. Le nom qu’utilisent vos appels. |
description | Texte libre, pour la prochaine personne qui lira la liste. |
default_max_age_seconds | La fenêtre de fraîcheur à appliquer quand un appel ne dit rien. |
config | La configuration de crawl à exécuter. Mêmes champs qu’un démarrage de crawl. |
from_crawl_id | Ou : copier la configuration d’un crawl déjà exécuté. |
Envoyez exactement un de config ou from_crawl_id. N’en envoyer aucun répond 400 — la définition n’aurait rien à exécuter.
L’exécuter
Le corps d’exécution tient en quatre champs, tous facultatifs :
max_age | 0 seconde ou plus. La fenêtre de fraîcheur pour cet appel. Accepté aussi en ?max_age= ou en Cache-Control max-age. |
wait | false par défaut. À true, l’appel HTTP bloque jusqu’au verdict du crawl. |
timeout | 5–300 secondes, 120 par défaut. N’a de sens qu’avec wait. |
limit | 1–500, 50 par défaut. Combien de lignes collectées reviennent en ligne. |
Le contrat de fraîcheur
C’est la partie contre laquelle coder. Le code de statut dit ce qui s’est passé, et aucune réponse n’est une impasse :
| Réponse | Ce qui s’est passé |
|---|---|
200 · cached: true | La dernière exécution était dans la fenêtre. Ses données reviennent en ligne. Rien n’a été crawlé, rien n’a été décompté. |
202 | Manqué. Un crawl frais a démarré ; le corps porte le crawl et son status_url. Interrogez-le. |
504 | wait: true a dépassé son timeout. Le crawl tourne encore et le corps porte toujours crawl_id et status_url — récupérez-le, ne réessayez pas. |
Cache-Control: no-cache · max_age: 0 | Recrawle toujours, quoi que dise la valeur par défaut de la définition. |
Chaque réponse porte un objet _cache — hit, age_seconds et source_crawl_id — pour qu’un client puisse journaliser pourquoi il a reçu ce qu’il a reçu. Et GET /v1/crawl/definitions/{ref}/data est une lecture pure de la dernière exécution terminée, à tout âge : elle ne crawle jamais et ne facture jamais.
Enregistrer, appeler, relire
Trois appels : créer la définition, l’exécuter avec une fenêtre, lire ce qu’elle contient déjà.
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" documents ▸ ocr
PDF, tableurs et pages scannées.
Un site est rarement du HTML seul. Les PDF, les fichiers Word, Excel et PowerPoint, les images et les pages scannées sont lus dans le cadre du crawl, avec OCR là où le texte n’est que des pixels. Un seul bouton le règle :
| ocr_mode | Effet |
|---|---|
auto | L’OCR intervient quand une page ou un document n’a pas de couche de texte lisible. |
off | Jamais d’OCR. Les documents avec couche de texte restent lus. |
force | OCR sur chaque page, même quand une couche de texte existe. |
Où cela s’applique : les crawls exécutés sur la flotte Writ Cloud, et les crawls auto-hébergés. Un crawl cloud que vous routez vers votre propre machine reliée ne fait pas d’extraction de documents — cette voie lit des pages, pas des documents. Choisissez le lieu d’exécution en conséquence. Les pages OCR comptent 2×.
prix ▸ à la page
Une page vaut 0,0005 $. Certaines pèsent plus.
Le crawl se paie à l’usage, à la page. Dans les pages de crawl mensuelles de votre plan, cela ne coûte rien de plus ; au-delà, c’est prélevé sur le wallet au même tarif, et un wallet qui ne peut pas couvrir la page répond 402 insufficient_credits. Les déploiements auto-hébergés crawlent sur vos propres agents et ne portent aucun frais de page de notre part.
| Comment la page a été lue | Poids |
|---|---|
| Page HTTP simple, ou document | 1× |
| Rendu navigateur | 2× |
| Page OCR | 2× |
| executor: "ai" | 5× |
Ce que chaque plan inclut
Trois nombres distincts par plan. Lisez-les comme trois, pas comme un — ils répondent à trois questions différentes.
| Plan | Pages de crawl par mois | Pages dans un crawl | Crawls simultanés |
|---|---|---|---|
| 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 |
Ce sont des nombres différents. « Pages dans un crawl » est le plafond d’un seul job ; « pages de crawl par mois » est ce que votre plan inclut sur tous les jobs de la période. Sur Pro, c’est 25 000 et 75 000 — le même plan fait trois crawls pleine taille par mois avant toute facturation.
Ils échouent aussi différemment. Un page_budget plus grand que votre plafond par crawl est ramené au plafond et le crawl s’exécute — on ne vous refuse pas d’avoir demandé. Les deux barrières qui peuvent vraiment refuser sont la limite de simultanéité (un crawl de trop pendant que d’autres tournent) et l’allocation mensuelle une fois que le wallet ne couvre plus le dépassement.
Lire le compteur
GET /api/crawl/meta/usage rend votre position actuelle, pour qu’un client décide avant de dépenser :
pages_included_per_month | L’allocation du plan pour la période. |
pages_used_this_period · pages_remaining | Où vous en êtes. |
per_job_page_cap | Le plafond appliqué à un seul crawl. |
max_concurrent_crawls | Combien peuvent tourner en même temps. |
overage_price_micros_per_page | Ce que coûte une page au-delà de l’allocation. |
browser_page_units · ocr_page_units | Les multiplicateurs de poids, pour que votre estimation colle à la facture. |
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 ▸ sans compte
Sans compte ? Une page à la fois.
Le palier keyless lit des pages publiques sans compte ni clé. Un appareil est identifié par un en-tête d’identifiant client, et l’allocation est petite à dessein : elle existe pour qu’un SDK ou l’app desktop fonctionne avant l’inscription.
| Appel | Ce qu’il fait |
|---|---|
POST /v1/keyless/scrape | Markdown complet d’une page publique. Coûte 1 requête et 1 page. |
POST /v1/keyless/map | Jusqu’à 200 URL d’un site. Coûte 1 requête, 0 page. |
GET /v1/keyless/quota | Ce qu’il reste. Ne dépense rien. |
POST /v1/keyless/crawl | Toujours 402 api_key_required — crawler un site entier demande un compte. |
Les plafonds
- 10 requêtes et 20 pages par jour, par appareil.
- 30 requêtes par jour et 10 par minute, par adresse IP.
- Une carte rend au plus 200 URL.
- Au-delà de l’un d’eux : 429 keyless_rate_limited.
Chaque appel keyless porte X-Writ-Client-Id — un identifiant d’appareil stable. Les SDKs et l’app desktop le posent pour vous ; un appel sans lui répond 400 client_id_required.
clés ▸ scopes
Trois scopes, volontairement inégaux.
Lire une page est une chose plus petite que crawler un site, donc c’est un scope plus petit. Une clé confiée à un partenaire pour des lectures une-page ne peut pas démarrer un crawl sur votre allocation.
| Scope | Ce qu’il donne |
|---|---|
crawl:execute | Démarrer et annuler des crawls ; créer, modifier et exécuter des crawls enregistrés. |
crawl:read | Lister et lire les crawls, les crawls enregistrés et leurs données collectées. |
scrape:execute | Lectures une-page, carte et preview. Séparé, et moindre. |
mcp ▸ outils
Le même crawl, en outils MCP.
Le serveur MCP desktop expose la surface de crawl en outils, pour qu’un client IA lance et relance un crawl sans que vous écriviez le moindre HTTP :
| Outil | Ce qu’il fait |
|---|---|
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 | Où en est un crawl en cours. |
writ_saved_crawls | Les crawls enregistrés que vous pouvez appeler par leur nom. |
writ_run_saved_crawl | crawl, max_age, limit — le contrat de fraîcheur, en outil. |
writ_saved_crawl_data | Ce qu’un crawl enregistré a déjà collecté. Ne crawle jamais. |
writ_scrape · writ_map | Une page, ou la liste d’URL. |
Deux comportements à connaître. Réutiliser un nom save_as met à jour ce crawl enregistré au lieu d’en créer un second — un client IA qui affine son crawl vous laisse donc une définition, pas douze. Et max_age n’a de sens qu’avec save_as : sans crawl enregistré, il n’y a pas d’exécution précédente à réutiliser.
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" } référence ▸ suite
Ce qui vient après le crawl
Chaque endpoint de crawl, champ par champ.
→ Datasets et fichiersLire, fouiller et exporter ce qu’un crawl a collecté.
→ MCPLes outils de crawl dans n’importe quel client MCP.
→ ScribeDécrivez le crawl en mots et laissez-le construire le périmètre.
→ Où ça tourneFlotte cloud, votre propre machine, ou auto-hébergé.
→ Facturation et usageComment pages, temps d’exécution et tokens IA sont décomptés ensemble.
→faq
Questions crawl, répondues.
Quelle différence entre le plafond de pages par crawl et l’allocation mensuelle ?
Comment éviter de payer un crawl que j’ai déjà lancé ?
Mon appel wait:true a rendu 504. Ai-je perdu les pages ?
Pourquoi les booléens d’un crawl reviennent-ils en 0 et 1 ?
Les PDF et les pages scannées sont-ils inclus ?
Puis-je crawler un site entier sans compte ?
fin ▸ en lancer un
Prévisualisez le périmètre, puis lancez.
Le preview ne coûte rien et montre exactement quelles URL un crawl garderait. C’est la façon la moins chère d’être sûr avant un gros budget de pages.