Crawling API
Récupérez n'importe quelle URL via le réseau de proxies résidentiels de Crawlbase, avec rendu JavaScript optionnel, résolution des défis anti-bot et routage géographique. Le point de terminaison polyvalent qui alimente tout le reste.
Fonctionnement
Chaque requête au Crawling API prend une URL cible et retourne la page que cette cible aurait servie à un véritable navigateur dans la bonne zone géographique, avec le bon profil d'appareil, après résolution des défis anti-bot. Trois étapes s'enchaînent à chaque appel :
- Routage. La requête est envoyée via un nœud de sortie résidentiel ou datacenter - automatiquement par défaut, ou dans un pays spécifique si vous passez
country=. Des sessions persistantes sont disponibles afin qu'une séquence d'appels réutilise la même IP. - Rendu. Si vous vous authentifiez avec un JavaScript token, l'URL est chargée dans un véritable navigateur headless. Les contrôles de page-wait, scroll, click et AJAX-idle vous permettent d'attendre le contenu réel plutôt que la coquille HTML initiale.
- Contournement anti-bot. Cloudflare, PerimeterX, DataDome, hCaptcha et autres défis courants sont résolus côté serveur. Vous obtenez le HTML post-défi, pas la page de défi.
Le même point de terminaison couvre les trois cas. Ne passez que les paramètres dont vous avez besoin : il n'y a pas d'« API de rendu JS » ni d'« API anti-bot » séparée. Si vous ne passez pas de paramètres réservés au JS token, la requête emprunte le chemin économique et rapide ; dès que vous le faites, la requête bascule sur le chemin de rendu. La tarification est la même par réponse réussie dans les deux cas.
Tokens
L'authentification utilise l'un des deux types de tokens : tous deux résident sur un même compte et authentifient le même point de terminaison :
- Normal Token (TCP) : pour les réponses HTML ou JSON statiques où vous n'avez pas besoin d'un navigateur. Plus rapide, moins coûteux, utilisé pour la majorité des cibles de scraping simples.
- JavaScript Token : pour les SPA, les applications React/Vue/Angular, les flux en lazy-loading et toute cible qui masque le contenu derrière un rendu côté client. Requis pour utiliser
page_wait,ajax_wait,scrolletcss_click_selector.
Si une requête avec le Normal token retourne un corps vide ou un 525 (défi non résolu), la solution standard est de réessayer avec le JavaScript token : la plupart des cibles modernes ont besoin d'un navigateur même quand leur HTML initial paraît complet. Voir Authentication pour le flux complet de gestion des tokens.
Concurrence & tarification
Chaque requête qui retourne pc_status: 200 est décomptée de votre quota mensuel. Les requêtes échouées (timeouts, blocages, 5xx provenant de la cible) sont gratuites : les nouvelles tentatives contre un upstream instable ne font pas grimper votre facture. Les limites de concurrence évoluent avec votre plan ; la réponse inclut un en-tête remaining que vous pouvez utiliser pour ralentir proactivement avant d'atteindre la limite. Les crawls de longue durée (rendu JS lourd, page_wait élevé) devraient utiliser le mode async ci-dessous pour libérer le créneau de concurrence dès que la requête est mise en file d'attente.
Timeouts côté client. Le temps de réponse moyen est de 4 à 10 secondes par requête, mais les requêtes en queue de latence (SPAs lourdes, scroll_interval=60, sites cibles lents) peuvent prendre plus de temps. Réglez votre timeout client à au moins 90 secondes pour que les réponses lentes mais légitimes ne dépassent pas le délai avant d'arriver.
Autres recommandations côté client. Envoyez Accept-Encoding: gzip sur chaque requête : les charges utiles sont non triviales (pages HTML complètes ou markdown) et gzip les réduit généralement à un tiers de leur taille sur le réseau. Si vous utilisez Scrapy, désactivez le cache DNS pour que l'hôte de l'API reste résoluble sur des crawls de longue durée.
Point de terminaison
- Méthodes :
GET(query uniquement),POST(corps form ou JSON),PUT(payload brut). - Le paramètre
urldoit être entièrement URL-encodé. - Le body renvoyé correspond au contenu de la page cible (HTML, JSON, image, etc).
- Les métadonnées sont renvoyées sous forme d'en-têtes de réponse (
pc_status,original_status,url,rid).
Démarrage rapide
curl 'https://api.crawlbase.com/?token=YOUR_TOKEN&url=https%3A%2F%2Fgithub.com%2Fanthropic'from crawlbase import CrawlingAPI
api = CrawlingAPI({'token': 'YOUR_TOKEN'})
res = api.get('https://github.com/anthropic')
print(res['body'])const { CrawlingAPI } = require('crawlbase');
const api = new CrawlingAPI({ token: 'YOUR_TOKEN' });
const res = await api.get('https://github.com/anthropic');
console.log(res.body);require 'crawlbase'
api = Crawlbase::API.new(token: 'YOUR_TOKEN')
res = api.get('https://github.com/anthropic')
puts res.body<?php
use Crawlbase\CrawlingAPI;
$api = new CrawlingAPI(['token' => 'YOUR_TOKEN']);
$res = $api->get('https://github.com/anthropic');
echo $res->body;package main
import (
"fmt"
"github.com/crawlbase/crawlbase-go"
)
func main() {
api, _ := crawlbase.NewCrawlingAPI("YOUR_TOKEN")
res, _ := api.Get("https://github.com/anthropic", nil)
fmt.Println(res.Body)
}Requête
Chaque requête au Crawling API est un seul appel HTTP vers le point de terminaison. La plupart des requêtes sont des GET : passez les paramètres de requête ci-dessous pour contrôler le rendu, la géolocalisation, le format de sortie et le comportement async. Utilisez POST lorsque vous devez envoyer un formulaire ou un corps JSON, et PUT pour les uploads de charges utiles brutes.
Paramètres de requête
Tous les paramètres sont passés en valeurs de query string. Seuls token et url sont requis.
Requis
http:// ou https://).Routage & géolocalisation
Choisissez l'origine de la requête et le type d'appareil que voit la cible. Le routage importe pour les boutiques en ligne, les SERP et tout site qui localise son contenu par IP : le catalogue Amazon allemand n'est pas accessible depuis une sortie américaine même avec la bonne URL, et les SERP de Google sont localisées à la fois par géographie et par les paramètres d'URL hl/gl combinés à l'IP. Définissez country explicitement et la bonne devise, langue et offre apparaissent automatiquement.
US, GB, DE, JP, …) pour acheminer le crawl via les nœuds de sortie de ce pays. Par défaut, sélection géographique automatique..onion. À laisser désactivé pour toute cible sur le clearnet : les sorties Tor sont plus lentes et plus bruyantes que le pool résidentiel.Crawlbase peut écraser le paramètre country pour sélectionner automatiquement un proxy en fonction de l'URL : cela donne le meilleur taux de succès sur la plupart des sites. Contactez le support si vous devez désactiver la sélection automatique de proxy.
Spécifier un pays peut réduire le nombre de requêtes réussies, ne l'utilisez donc que lorsque la géolocalisation importe réellement pour la page que vous crawlez. Certains sites (notamment Amazon) sont routés via des proxies dédiés indépendamment du pays que vous passez : tous les pays sont autorisés pour ces domaines même s'ils ne figurent pas dans la liste prise en charge ci-dessous.
Vous avez accès aux pays suivants :
| Australie (AU) | Brésil (BR) | Canada (CA) |
| Suisse (CH) | Chine (CN) | Allemagne (DE) |
| Espagne (ES) | Finlande (FI) | France (FR) |
| Royaume-Uni (GB) | Inde (IN) | Japon (JP) |
| Mexique (MX) | Pays-Bas (NL) | Norvège (NO) |
| Pologne (PL) | Russie (RU) | Seychelles (SC) |
| Suède (SE) | Turquie (TR) | Ukraine (UA) |
| États-Unis (US) |
En-têtes & cookies
Transférez vos propres en-têtes de requête et cookies vers le site cible, ou épinglez une session persistante pour que les valeurs Set-Cookie d'un appel soient rejouées au suivant. Utile lorsque la cible a besoin d'un Accept-Language, d'un cookie CSRF ou d'une session connectée qui doit survivre à travers les requêtes d'un flux.
accept-language:en-GB|accept-encoding:gzip. À combiner avec get_headers=true pour faire également remonter les en-têtes de réponse de la cible.Cookie : key1=value1; key2=value2.En-têtes autorisés. Tous les en-têtes que vous passez via request_headers n'atteindront pas forcément le site cible : Crawlbase en supprime un petit ensemble par défaut. Pour vérifier ce qui est réellement envoyé, envoyez une requête de test à https://postman-echo.com/headers et inspectez ce que le service d'écho reçoit. Si vous avez besoin qu'un en-tête supplémentaire soit autorisé pour votre token, contactez le support avec le ou les noms d'en-têtes.
Rendu JavaScript
Ces paramètres requièrent un JavaScript token. Ils contrôlent comment le navigateur headless attend le contenu avant de capturer le DOM. Si vous vous retrouvez à en utiliser plusieurs à la fois, l'ordre à garder en tête est : page_wait en premier (un délai fixe pour les animations prévisibles), puis ajax_wait (laissez tomber le délai fixe si la page émet des requêtes réseau après le mount), puis scroll (uniquement si le contenu dont vous avez besoin se trouve sous la ligne de flottaison), puis css_click_selector (uniquement si un bouton ou un accordéon verrouille les données).
Un piège fréquent : régler page_wait trop haut « au cas où ». Chaque milliseconde supplémentaire représente de la concurrence que vous ne pouvez pas utiliser ailleurs. Commencez à 0, n'augmentez que lorsque vous constatez une sortie tronquée, et considérez ajax_wait comme une alternative plus intelligente : il retourne dès que le réseau devient inactif plutôt que de bloquer sur un délai fixe.
scroll=true.screenshot_url dans les en-têtes de réponse (ou dans le corps JSON quand format=json) et expire après une heure. Pour les workflows multi-captures ou pleine page, utilisez plutôt l'API Screenshots dédiée.Options de sortie de la capture d'écran. Quand screenshot=true, la capture par défaut est la page rendue complète. Pour la limiter au seul viewport, ajoutez mode=viewport ; combinez-le avec width et height (en pixels) pour contraindre la capture. Les deux ont par défaut les dimensions de l'écran et ne prennent effet qu'avec mode=viewport. Exemple : &screenshot=true&mode=viewport&width=1200&height=800.
Comment scroll est facturé. Les requêtes avec scroll activé sont facturées au temps total de traitement côté serveur. Les 8 premières secondes (chargement de page + défilement combinés) comptent pour 1 requête ; chaque tranche de 5 secondes supplémentaires ajoute 1 requête facturée. Un défilement de 20 s = 1 (les 8 premières secondes) + 1 (9–13 s) + 1 (14–18 s) + 1 (19–20 s, les blocs partiels comptent comme entiers) = 4 requêtes facturées. Si la page se termine avant scroll_interval, seul le temps de traitement réel est facturé.
Le scroll_interval maximum est de 60 secondes : au-delà de 60s, le défilement s'arrête et la réponse est renvoyée. Lorsque vous définissez scroll_interval=60, gardez la connexion côté client ouverte pendant au moins 90 secondes pour que la réponse ait le temps de revenir. Combiner scroll avec page_wait augmente le temps total de traitement et donc le nombre de requêtes facturées.
Le paramètre css_click_selector ne prend effet que lorsque vous utilisez le JavaScript token (il s'exécute à l'intérieur du navigateur headless avant la capture du DOM). Il accepte tout sélecteur CSS valide et entièrement spécifié : par exemple un ID comme #some-button, une classe comme .some-other-button, ou un sélecteur d'attribut comme [data-tab-item="tab1"]. Encodez toujours la valeur en URL pour que les caractères spéciaux survivent intacts à la query string.
Si le sélecteur n'est pas trouvé sur la page, la requête échoue avec pc_status 595. Pour recevoir tout de même une réponse lorsque la cible du clic peut être absente, ajoutez un sélecteur universellement présent en repli, séparé par une virgule. Par exemple #some-button,body retombe sur un clic sur body lorsque #some-button n'existe pas.
Sélecteurs multiples. Pour cliquer sur plusieurs éléments en séquence avant la capture, séparez-les par une barre verticale (|). URL-encodez la valeur entière, y compris la barre. Par exemple, cliquer sur #start-button puis sur .next-page-link ressemble à #start-button|.next-page-link en forme brute, ou %23start-button%7C.next-page-link URL-encodé. Les clics ont lieu dans l'ordre indiqué. Si un sélecteur de la chaîne est absent, la même règle de pc_status 595 s'applique, donc le pattern de fallback ,body fonctionne par sélecteur.
Besoin d'exécuter du JavaScript personnalisé dans la page avant que Crawlbase ne capture le DOM (par ex. dispatcher un événement synthétique, modifier un état, forcer un fetch) ? C'est une fonctionnalité activable au cas par cas selon votre usage : contactez le support en décrivant ce que vous cherchez à faire et nous le mettrons en place.
Async & stockage
Le mode async fait passer l'API de « bloque jusqu'à ce que ma page soit prête » à « mets ça en file et préviens-moi quand c'est terminé ». Le point de terminaison retourne immédiatement un rid ; le résultat réel est livré à un webhook que vous spécifiez, ou stocké dans Cloud Storage et récupéré plus tard via le même rid. C'est le bon mode pour les jobs par lots et les cibles lentes : l'async libère votre créneau de concurrence dès que la requête est mise en file, vous pouvez donc continuer à soumettre pendant que les crawls tournent encore. Pour les jobs à très haut volume (millions d'URLs), utilisez l'Enterprise Crawler qui s'appuie sur ce même pipeline async avec retries, gestion du rythme et livraison des résultats.
Le flag async=true n'est actuellement pris en charge que pour les URLs linkedin.com. Si vous avez besoin de crawls async sur d'autres domaines, contactez le support en indiquant le domaine cible afin que nous l'activions pour votre token.
rid au lieu de bloquer. Le résultat est livré au callback s'il est défini, ou disponible via Cloud Storage par rid.async=true si vous ne voulez pas faire de polling.rid en plus du corps.Format de sortie
La réponse par défaut est le corps brut de la page : exactement ce qu'un navigateur recevrait après rendu et résolution anti-bot. Pour la plupart des pipelines, c'est la bonne forme (votre parseur en aval gère directement le HTML). Utilisez format=json lorsque vous voulez les métadonnées (statut, URL finale, RID, en-têtes) regroupées dans une seule enveloppe plutôt que réparties entre les en-têtes de réponse et le corps. Utilisez scraper= ou autoparse=true lorsque la cible est l'une de celles pour lesquelles nous avons déjà un parseur : vous sautez complètement l'étape d'analyse et recevez des champs structurés propres au lieu du balisage brut.
html retourne la page brute avec les métadonnées dans les en-têtes de réponse. json emballe la page plus toutes les métadonnées dans un seul objet JSON. md convertit la page en GitHub-Flavored Markdown : associez-le à md_readability=true pour retirer d'abord la navigation, la barre latérale et les pubs.format=md. Lorsque la valeur est true, Crawlbase effectue une passe de lisibilité sur la page avant la conversion en Markdown : supprime les éléments de chrome (navigation, barre latérale, pied de page, emplacements publicitaires) et conserve le contenu principal de l'article. Idéal pour convertir des billets de blog et des articles en contexte LLM propre.format=json. Pretty-print l'enveloppe JSON avec indentation et retours à la ligne pour la lecture humaine ; à laisser désactivé en production pour garder des réponses compactes.amazon-product-details.Contrôle de la réponse
Ces paramètres modifient ce que contient la réponse ou la façon dont Crawlbase décide qu'une requête a réussi. Utilisez get_headers et get_cookies lorsque vous avez besoin de récupérer les en-têtes de réponse du site cible ou les valeurs Set-Cookie (ils sont supprimés par défaut). Utilisez custom_success_codes lorsque la cible retourne légitimement un statut non-2xx que votre pipeline doit traiter comme une récupération propre : sans cela, Crawlbase réessaiera ces réponses à votre place.
original_header_*, ou regroupés sous original_headers quand format=json.Set-Cookie du site cible. Elles reviennent en tant que original_set_cookie dans les en-têtes de réponse, ou sous la même clé quand format=json.custom_success_codes=403,429,503. Crawlbase ne réessaiera pas ces requêtes, et le statut d'origine est conservé dans original_status. À utiliser lorsque la cible retourne légitimement ces codes pour votre point de terminaison (API protégées par authentification, pages bloquées géographiquement dont vous voulez quand même le corps).Requêtes POST
Utilisez POST lorsque le point de terminaison cible attend un corps de requête : soumissions de formulaire, API JSON, GraphQL, tout ce qui ne tient pas dans une query string. Même point de terminaison, mêmes paramètres, même forme de réponse que GET ; seuls la méthode HTTP et le corps changent.
Les requêtes POST fonctionnent uniquement avec le Normal token. Le JavaScript token (et les paramètres de rendu JS page_wait, ajax_wait, scroll, css_click_selector) sont réservés à GET : lorsque vous devez soumettre un formulaire sur une page rendue en JS, utilisez le JavaScript token avec css_click_selector pour piloter le bouton du formulaire plutôt que de POSTer directement sur l'URL du formulaire.
Le Content-Type par défaut est application/x-www-form-urlencoded. Passez les champs du formulaire comme corps de la requête : Crawlbase les transmet à la cible inchangés.
curl 'https://api.crawlbase.com/?token=YOUR_TOKEN' \
--data-urlencode 'url=https://postman-echo.com/post' -G \
-F 'parameter1=testing some post data' \
-F 'parameter2=here goes some data'import requests
from urllib.parse import quote_plus
url = quote_plus('https://postman-echo.com/post')
res = requests.post(
f'https://api.crawlbase.com/?token=YOUR_TOKEN&url={url}',
data={'parameter1': 'value', 'parameter2': 'another value'},
)
print(res.status_code, res.text)const url = encodeURIComponent('https://postman-echo.com/post');
const body = new URLSearchParams({ parameter1: 'value', parameter2: 'another' });
const res = await fetch(`https://api.crawlbase.com/?token=YOUR_TOKEN&url=${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
console.log(res.status, await res.text());require 'net/http'
uri = URI('https://api.crawlbase.com')
uri.query = URI.encode_www_form(token: 'YOUR_TOKEN', url: 'https://postman-echo.com/post')
res = Net::HTTP.post_form(uri, 'parameter1' => 'value', 'parameter2' => 'another')
puts res.code, res.body<?php
$url = 'https://postman-echo.com/post';
$body = http_build_query(['parameter1' => 'value', 'parameter2' => 'another']);
$ch = curl_init('https://api.crawlbase.com/?token=YOUR_TOKEN&url=' . urlencode($url));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);package main
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
func main() {
target := url.QueryEscape("https://postman-echo.com/post")
body := strings.NewReader("parameter1=value¶meter2=another")
res, _ := http.Post(
"https://api.crawlbase.com/?token=YOUR_TOKEN&url="+target,
"application/x-www-form-urlencoded",
body,
)
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}POST ne peut pas être utilisé pour spammer ni nuire de quelque manière que ce soit aux sites cibles. Crawlbase surveille activement les schémas abusifs ; les comptes pris à utiliser POST pour du spam, du credential stuffing ou tout autre trafic malveillant seront suspendus et signalés.
POST avec un corps JSON
Remplacez le Content-Type form-urlencoded par défaut avec post_content_type. URL-encodez la valeur (par ex. application/json devient application%2Fjson). Le corps est transmis à la cible inchangé : encodez-le en JSON vous-même.
curl 'https://api.crawlbase.com/?token=YOUR_TOKEN' \
--data-urlencode 'url=https://postman-echo.com/post' \
--data-urlencode 'post_content_type=application/json;charset=UTF-8' -G \
--request POST \
--data '{"param1":"value","param2":"another"}'import json, requests
from urllib.parse import quote_plus
url = quote_plus('https://postman-echo.com/post')
res = requests.post(
f'https://api.crawlbase.com/?token=YOUR_TOKEN'
f'&url={url}'
f'&post_content_type=application/json',
data=json.dumps({'param1': 'value', 'param2': 'another'}),
headers={'Content-Type': 'application/json'},
)
print(res.status_code, res.text)const url = encodeURIComponent('https://postman-echo.com/post');
const ct = encodeURIComponent('application/json;charset=UTF-8');
const body = JSON.stringify({ param1: 'value', param2: 'another' });
const res = await fetch(
`https://api.crawlbase.com/?token=YOUR_TOKEN&url=${url}&post_content_type=${ct}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body },
);
console.log(res.status, await res.text());require 'net/http'
require 'json'
uri = URI('https://api.crawlbase.com')
uri.query = URI.encode_www_form(
token: 'YOUR_TOKEN',
url: 'https://postman-echo.com/post',
post_content_type: 'application/json'
)
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = { param1: 'value', param2: 'another' }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.code, res.body<?php
$url = 'https://postman-echo.com/post';
$ct = urlencode('application/json;charset=UTF-8');
$body = json_encode(['param1' => 'value', 'param2' => 'another']);
$ch = curl_init(
'https://api.crawlbase.com/?token=YOUR_TOKEN'
. '&url=' . urlencode($url)
. '&post_content_type=' . $ct
);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);package main
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
target := url.QueryEscape("https://postman-echo.com/post")
ct := url.QueryEscape("application/json;charset=UTF-8")
body := bytes.NewBufferString(`{"param1":"value","param2":"another"}`)
res, _ := http.Post(
"https://api.crawlbase.com/?token=YOUR_TOKEN&url="+target+"&post_content_type="+ct,
"application/json",
body,
)
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}Note : c'est le site cible qui décide d'accepter ou non le corps. Crawlbase transmet la requête honnêtement : si la cible retourne 4xx parce que la forme du corps est incorrecte, cela apparaît dans original_status, pas dans pc_status. Voir Errors pour le schéma de branchement.
Requêtes PUT
PUT fonctionne de la même façon que POST : même point de terminaison, mêmes paramètres, mêmes règles d'encodage du corps. La seule différence est la méthode HTTP.
curl 'https://api.crawlbase.com/?token=YOUR_TOKEN' \
--data-urlencode 'url=https://api.example.com/resource/42' -G \
--request PUT \
--header 'Content-Type: application/json' \
--data '{"name":"updated","status":"active"}'import requests
from urllib.parse import quote_plus
url = quote_plus('https://api.example.com/resource/42')
res = requests.put(
f'https://api.crawlbase.com/?token=YOUR_TOKEN&url={url}&post_content_type=application/json',
data='{"name":"updated","status":"active"}',
headers={'Content-Type': 'application/json'},
)
print(res.status_code, res.text)const url = encodeURIComponent('https://api.example.com/resource/42');
const ct = encodeURIComponent('application/json');
const body = JSON.stringify({ name: 'updated', status: 'active' });
const res = await fetch(
`https://api.crawlbase.com/?token=YOUR_TOKEN&url=${url}&post_content_type=${ct}`,
{ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body },
);
console.log(res.status, await res.text());require 'net/http'
require 'json'
uri = URI('https://api.crawlbase.com')
uri.query = URI.encode_www_form(
token: 'YOUR_TOKEN',
url: 'https://api.example.com/resource/42',
post_content_type: 'application/json'
)
req = Net::HTTP::Put.new(uri, 'Content-Type' => 'application/json')
req.body = { name: 'updated', status: 'active' }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.code, res.body<?php
$url = 'https://api.example.com/resource/42';
$ct = urlencode('application/json');
$body = json_encode(['name' => 'updated', 'status' => 'active']);
$ch = curl_init(
'https://api.crawlbase.com/?token=YOUR_TOKEN'
. '&url=' . urlencode($url)
. '&post_content_type=' . $ct
);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);package main
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
target := url.QueryEscape("https://api.example.com/resource/42")
ct := url.QueryEscape("application/json")
body := bytes.NewBufferString(`{"name":"updated","status":"active"}`)
req, _ := http.NewRequest(
"PUT",
"https://api.crawlbase.com/?token=YOUR_TOKEN&url="+target+"&post_content_type="+ct,
body,
)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}Comme POST, PUT nécessite le Normal token. Utilisez post_content_type pour contrôler le type de média du corps s'il n'est pas en form-urlencoded.
Crawlbase surveille activement le trafic POST et PUT. Envoyer des corps de requête ciblant des sites tiers qui ne vous appartiennent pas - spam de commentaires, soumissions de formulaire frauduleuses, création de comptes scriptée - entraîne la suspension du compte d'origine dès la première détection. Utilisez ces verbes pour des intégrations API légitimes, vos propres environnements de staging et de production, ainsi que l'automatisation explicitement autorisée.
Réponse
Les réponses réussies retournent la page cible dans le corps. Les métadonnées se trouvent dans les en-têtes de réponse.
En-têtes
| En-tête | Description |
|---|---|
pc_status | Code de statut Crawlbase. 200 = succès. |
original_status | Statut HTTP du site cible. |
url | URL finale après redirections. |
rid | ID de requête. Retourné quand async=true ou store=true. |
content-type | Type MIME du corps (text/html, application/json, image/png, etc). |
original_header_* | Retourné quand get_headers=true. Chaque en-tête du site cible arrive avec un préfixe original_header_ (par exemple original_header_x_frame_options). Regroupés sous original_headers quand format=json. |
screenshot_url | Retourné quand screenshot=true. URL JPEG temporaire de la page rendue ; expire une heure après le crawl. |
original_set_cookie | Retourné quand get_cookies=true. Valeurs Set-Cookie concaténées issues de la réponse du site cible. |
domain_complexityégalement X-Domain-Complexity | Le niveau de complexité du domaine crawlé : l'une des valeurs standard, moderate ou complex. Reflète les ressources nécessaires pour contourner les protections du site et correspond directement au niveau tarifaire facturé pour la requête. Voir les niveaux de complexité ci-dessous. |
storage_url | Retourné quand la requête a été faite avec store=true. Pointeur vers la copie stockée de la réponse dans Crawlbase Cloud Storage ; à associer avec rid pour la récupérer plus tard. |
Content-Type | text/markdown; charset=utf-8 quand la requête a été faite avec format=md ; text/html ou application/json standard sinon. |
X-Markdown-Flavor | Dialecte Markdown du corps de réponse : actuellement GitHub Flavored Markdown (GFM). Émis uniquement lorsque format=md. |
X-Markdown-Features | Liste séparée par des virgules des fonctionnalités GFM utilisées dans le corps (par exemple tables,lists). Vous permet de choisir un parser avec les bonnes extensions activées. Émis uniquement quand format=md. |
X-Markdown-Base-URL | Hôte de l'URL résolue (après d'éventuelles redirections). Utile pour résoudre les liens relatifs dans le corps markdown. Émis uniquement quand format=md. |
X-Markdown-Generator | Identifie le convertisseur : la valeur est ProxyCrawl-API. Émis uniquement lorsque format=md. |
Réponse HTML
Le comportement par défaut. format=html (ou aucun format du tout) retourne le corps brut de la page dans le corps HTTP, avec les métadonnées dans les en-têtes de réponse (url, original_status, pc_status, X-Domain-Complexity, plus toutes les entrées original_header_* auxquelles vous avez souscrit via get_headers=true).
GET 'https://api.crawlbase.com/?token=YOUR_TOKEN&url=https%3A%2F%2Fgithub.com%2Fcrawlbase&format=html'
Response:
Headers:
url: https://github.com/crawlbase
original_status: 200
pc_status: 200
X-Domain-Complexity: standard
Body:
<!doctype html><html>
<head>...</head>
<body>... (full page HTML) ...</body>
</html>Réponse JSON
Définissez format=json pour récupérer les mêmes données sous forme d'un seul objet JSON :
GET 'https://api.crawlbase.com/?token=YOUR_TOKEN&url=https%3A%2F%2Fgithub.com%2Fcrawlbase&format=json'
Response:
{
"original_status": 200,
"pc_status": 200,
"url": "https://github.com/crawlbase",
"domain_complexity": "standard",
"body": "<!doctype html><html>... (full page HTML) ...</html>"
}Réponse Markdown
format=md retourne la page déjà convertie en GitHub Flavored Markdown dans le corps, avec Content-Type: text/markdown; charset=utf-8 et un bloc d'en-têtes de métadonnées X-Markdown-* (Flavor, Features, Base-URL, Generator) à côté des habituels url / original_status / pc_status. Associez-le à md_readability=true lorsque vous voulez une extraction du contenu principal (corps de l'article, sans chrome) avant la conversion : voir le paramètre md_readability.
GET 'https://api.crawlbase.com/?token=YOUR_TOKEN&url=https%3A%2F%2Fgithub.com%2Fcrawlbase&format=md'
Response:
Headers:
Content-Type: text/markdown; charset=utf-8
X-Markdown-Flavor: GitHub Flavored Markdown (GFM)
X-Markdown-Features: tables,lists
X-Markdown-Base-URL: github.com
X-Markdown-Generator: ProxyCrawl-API
url: https://github.com/crawlbase
original_status: 200
pc_status: 200
Body:
# crawlbase
... (markdown text of the page) ...Requêtes facturables
Crawlbase ne facture que les requêtes où pc_status est 200 et où original_status fait partie de :
| Code | Signification |
|---|---|
200 | OK |
201 | Créé |
204 | Pas de contenu |
301 | Déplacé définitivement |
302 | Found - uniquement lorsque la redirection a été suivie et a retourné du contenu |
404 | Introuvable |
410 | Disparu |
Tout autre original_status est gratuit, et il en va de même pour tout pc_status autre que 200. Utilisez cette liste lors du rapprochement d'une facture d'utilisation avec les logs de votre application.
Niveaux de complexité de domaine
Le champ domain_complexity (également retourné sous forme d'en-tête de réponse X-Domain-Complexity) vous indique la difficulté du crawl sur le domaine cible, et le niveau tarifaire dans lequel la requête est tombée.
standard: facile à crawler, protection minimale. Niveau tarifaire le plus bas.moderate: protection anti-bot modérée nécessitant un traitement spécialisé. Niveau tarifaire intermédiaire.complex: protection avancée nécessitant des ressources spécialisées. Niveau tarifaire le plus élevé.
Pour la tarification spécifique à chaque palier, consultez votre plan d'abonnement ou contactez le service commercial.
Modèles courants
Recettes de bout en bout qui combinent les paramètres de requête ci-dessus dans les workflows que les équipes utilisent le plus souvent.
SPA rendue en JS avec défilement
curl 'https://api.crawlbase.com/?token=JS_TOKEN' \
--data-urlencode 'url=https://feed.example.com' \
--data-urlencode 'page_wait=2000' \
--data-urlencode 'scroll=true' \
--data-urlencode 'scroll_interval=15' -Gfrom crawlbase import CrawlingAPI
api = CrawlingAPI({'token': 'JS_TOKEN'})
res = api.get('https://feed.example.com', {
'page_wait': 2000,
'scroll': True,
'scroll_interval': 15,
})const { CrawlingAPI } = require('crawlbase');
const api = new CrawlingAPI({ token: 'JS_TOKEN' });
const res = await api.get('https://feed.example.com', {
page_wait: 2000,
scroll: true,
scroll_interval: 15,
});
console.log(res.body);require 'crawlbase'
api = Crawlbase::API.new(token: 'JS_TOKEN')
res = api.get('https://feed.example.com',
page_wait: 2000,
scroll: true,
scroll_interval: 15
)
puts res.body<?php
use Crawlbase\CrawlingAPI;
$api = new CrawlingAPI(['token' => 'JS_TOKEN']);
$res = $api->get('https://feed.example.com', [
'page_wait' => 2000,
'scroll' => true,
'scroll_interval' => 15,
]);
echo $res->body;package main
import (
"fmt"
"log"
"github.com/crawlbase/crawlbase-go"
)
func main() {
api, err := crawlbase.NewCrawlingAPI("JS_TOKEN")
if err != nil {
log.Fatal(err)
}
res, _ := api.Get("https://feed.example.com", map[string]string{
"page_wait": "2000",
"scroll": "true",
"scroll_interval": "15",
})
fmt.Println(res.Body)
}Requête routée géographiquement
# Route through Germany; the echoed JSON confirms the exit country
curl 'https://api.crawlbase.com/?token=YOUR_TOKEN' \
--data-urlencode 'url=https://ipinfo.io/json' \
--data-urlencode 'country=DE' -Gfrom crawlbase import CrawlingAPI
api = CrawlingAPI({'token': 'YOUR_TOKEN'})
res = api.get('https://ipinfo.io/json', {'country': 'DE'})
print(res['body'])const { CrawlingAPI } = require('crawlbase');
const api = new CrawlingAPI({ token: 'YOUR_TOKEN' });
const res = await api.get('https://ipinfo.io/json', { country: 'DE' });
console.log(res.body);require 'crawlbase'
api = Crawlbase::API.new(token: 'YOUR_TOKEN')
res = api.get('https://ipinfo.io/json', country: 'DE')
puts res.body<?php
use Crawlbase\CrawlingAPI;
$api = new CrawlingAPI(['token' => 'YOUR_TOKEN']);
$res = $api->get('https://ipinfo.io/json', ['country' => 'DE']);
echo $res->body;package main
import (
"fmt"
"log"
"github.com/crawlbase/crawlbase-go"
)
func main() {
api, err := crawlbase.NewCrawlingAPI("YOUR_TOKEN")
if err != nil {
log.Fatal(err)
}
res, _ := api.Get("https://ipinfo.io/json", map[string]string{
"country": "DE",
})
fmt.Println(res.Body)
}Crawl asynchrone avec webhook
Le mode async libère votre slot de concurrence dès que la requête est mise en file d'attente, ce qui évite qu'un long crawl monopolise votre budget. Utilisez-le pour les cibles lentes (JS lourd, page_wait long) lorsque vous devez traiter un volume élevé.
curl 'https://api.crawlbase.com/?token=YOUR_TOKEN' \
--data-urlencode 'url=https://example.com' \
--data-urlencode 'async=true' \
--data-urlencode 'callback=https://your-app.com/webhook' -G
# → returns immediately: { "rid": "a1B2c3D4e5F6" }
# → result POSTed to your callback when readyfrom crawlbase import CrawlingAPI
api = CrawlingAPI({'token': 'YOUR_TOKEN'})
res = api.get('https://example.com', {
'async': 'true',
'callback': 'https://your-app.com/webhook',
})
print(res['rid']) # → returned immediately; result POSTed to callback laterconst { CrawlingAPI } = require('crawlbase');
const api = new CrawlingAPI({ token: 'YOUR_TOKEN' });
const res = await api.get('https://example.com', {
async: true,
callback: 'https://your-app.com/webhook',
});
console.log(res.rid); // → returned immediately; result POSTed to callback laterrequire 'crawlbase'
api = Crawlbase::API.new(token: 'YOUR_TOKEN')
res = api.get('https://example.com',
async: true,
callback: 'https://your-app.com/webhook'
)
puts res.rid # → returned immediately; result POSTed to callback later<?php
use Crawlbase\CrawlingAPI;
$api = new CrawlingAPI(['token' => 'YOUR_TOKEN']);
$res = $api->get('https://example.com', [
'async' => 'true',
'callback' => 'https://your-app.com/webhook',
]);
echo $res->rid; // → returned immediately; result POSTed to callback laterpackage main
import (
"fmt"
"log"
"github.com/crawlbase/crawlbase-go"
)
func main() {
api, err := crawlbase.NewCrawlingAPI("YOUR_TOKEN")
if err != nil {
log.Fatal(err)
}
res, _ := api.Get("https://example.com", map[string]string{
"async": "true",
"callback": "https://your-app.com/webhook",
})
fmt.Println(res.RID) // → returned immediately; result POSTed to callback later
}Mode proxy
Le même Crawling API peut être invoqué en tant que proxy HTTP/HTTPS plutôt qu'en tant que point de terminaison REST : utile lorsque vous avez un scraper existant, un script d'automatisation de navigateur ou un client HTTP qui prend déjà en charge la configuration de proxy et que vous préférez glisser Crawlbase devant plutôt que de réécrire la couche de requête.
Pointez votre client vers smartproxy.crawlbase.com:8001 (HTTPS, recommandé) ou smartproxy.crawlbase.com:8000 (HTTP) et passez votre token comme nom d'utilisateur du proxy. Toutes les fonctionnalités du Crawling API - rendu JS, contournement anti-bot, routage géographique - s'appliquent à l'identique ; seule la forme de la requête diffère.
Mode proxy vs. Smart AI Proxy
Deux produits partagent le même nom d'hôte mais utilisent des ports différents : facile à confondre. Les capacités sont essentiellement les mêmes des deux côtés (routage par pays, émulation d'appareil, sessions, en-têtes personnalisés, rendu JS via les contrôles CrawlbaseAPI-*) ; ils diffèrent par l'abonnement sur lequel vous êtes facturé et le niveau de concurrence / threads fourni par cet abonnement :
- Crawling API en mode proxy (cette section) → ports
8000/8001. Passe par votre forfait Crawling API : même quota mensuel, même budget de concurrence, même facturation par succès que les appels en mode REST. Choisissez cette option si vous payez déjà pour le Crawling API et souhaitez disposer d'une interface de type proxy en complément du point de terminaison REST. - Smart AI Proxy (produit séparé, voir Smart AI Proxy) → ports
8012/8013. Un SKU distinct avec son propre abonnement et son propre modèle de threads / concurrence, dimensionné pour les pipelines de scraping proxy-first qui font déjà tourner un grand nombre de threads. Même réseau et mêmes en-têtes de contrôle en sous-couche : le choix porte sur le contrat et le profil de concurrence qui correspondent à votre usage.
Règle générale : choisissez le produit dont vous détenez déjà l'abonnement (ou dont le modèle tarifaire correspond à la forme de votre trafic). La surface fonctionnelle est la même ; les ports vous orientent simplement vers la bonne voie de facturation et de concurrence.
Démarrage rapide
Un premier appel depuis votre shell : Normal token, proxy HTTPS :
# HTTPS proxy (recommended)
curl -x 'https://[email protected]:8001' \
-k 'https://httpbin.org/ip'
# HTTP alternative
curl -x 'http://[email protected]:8000' \
-k 'https://httpbin.org/ip'import requests
proxies = {
'http': 'http://[email protected]:8000',
'https': 'http://[email protected]:8000',
}
res = requests.get('https://httpbin.org/ip', proxies=proxies, verify=False)
print(res.status_code, res.text)const { HttpsProxyAgent } = require('https-proxy-agent');
const agent = new HttpsProxyAgent('http://[email protected]:8000');
const res = await fetch('https://httpbin.org/ip', { agent });
console.log(res.status, await res.text());require 'net/http'
uri = URI('https://httpbin.org/ip')
http = Net::HTTP.new(uri.host, uri.port,
'smartproxy.crawlbase.com', 8000, 'YOUR_TOKEN', '')
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = http.get(uri.request_uri)
puts res.code, res.body<?php
$ch = curl_init('https://httpbin.org/ip');
curl_setopt($ch, CURLOPT_PROXY, 'smartproxy.crawlbase.com:8000');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'YOUR_TOKEN:');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
proxyURL, _ := url.Parse("http://[email protected]:8000")
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
res, _ := client.Get("https://httpbin.org/ip")
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}Pour les cibles rendues en JS, utilisez votre JavaScript token :
curl -x 'https://[email protected]:8001' \
-k 'https://spa.example.com'import requests
proxies = {
'http': 'http://[email protected]:8000',
'https': 'http://[email protected]:8000',
}
res = requests.get('https://spa.example.com', proxies=proxies, verify=False)
print(res.status_code)const { HttpsProxyAgent } = require('https-proxy-agent');
const agent = new HttpsProxyAgent('http://[email protected]:8000');
const res = await fetch('https://spa.example.com', { agent });
console.log(res.status);require 'net/http'
uri = URI('https://spa.example.com')
http = Net::HTTP.new(uri.host, uri.port,
'smartproxy.crawlbase.com', 8000, 'YOUR_JS_TOKEN', '')
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = http.get(uri.request_uri)
puts res.code<?php
$ch = curl_init('https://spa.example.com');
curl_setopt($ch, CURLOPT_PROXY, 'smartproxy.crawlbase.com:8000');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'YOUR_JS_TOKEN:');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);package main
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
)
func main() {
proxyURL, _ := url.Parse("http://[email protected]:8000")
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
res, _ := client.Get("https://spa.example.com")
fmt.Println(res.Status)
}Limites de débit
La limite de débit par défaut en mode proxy est de 20 requêtes par seconde (~1,7 M req/jour). Les clients basés sur la concurrence devraient raisonner en threads plutôt qu'en RPS : à la latence typique du Crawling API (~4 s pour une page produit Amazon), cela représente environ 80 threads concurrents. Les cibles plus rapides se traduisent par moins de threads.
Si vous atteignez le plafond, contactez le support en décrivant votre cas d'usage pour négocier une concurrence plus élevée.
Erreurs & tentatives
Le Crawling API expose deux codes de statut sur chaque réponse : original_status (ce que le site cible a retourné) et pc_status (ce que Crawlbase en a fait après application des règles anti-bot, de redirection et de validation du contenu). Ils peuvent diverger : une cible peut retourner 200 avec un corps vide, auquel cas original_status vaut 200 mais pc_status vaut 520. Branchez-vous toujours sur pc_status pour décider de réessayer ou non.
Les échecs spécifiques au Crawling API les plus fréquents :
| Code | Signification | Action |
|---|---|---|
422 | url manquant ou non URL-encodé | Encodez l'URL avant l'envoi. La plupart des clients (libcurl --data-urlencode, Python requests, Node fetch) le font automatiquement : mais les query strings construites à la main omettent souvent cette étape. |
520 | Réponse vide de la cible | Réessayez une fois. Si toujours vide, passez du Normal au JS token : de nombreux sites servent une coquille vide aux user-agents non-navigateur et s'appuient sur JS pour remplir la page. |
521 | Site cible hors service / inaccessible | Traitez-le comme une erreur amont transitoire. Backoff + nouvelle tentative ; si cela persiste sur plusieurs minutes, le site est réellement hors service. |
522 | Délai de connexion dépassé en atteignant la cible | Réessayez avec backoff. Essayez un autre country si la cible est instable selon la zone géographique. |
523 | Origine inaccessible depuis la sortie choisie | Réessayez sans country (laissez le routage automatique choisir) ou avec un autre pays. |
525 | Le défi anti-bot n'a pas pu être résolu | Passez du Normal au JS token. Si vous êtes déjà sur JS, réessayez ; si persistant, escaladez au support : cela signifie généralement que la cible a déployé une nouvelle variante de défi. |
595 | Sélecteur introuvable. La page s'est chargée correctement mais le sélecteur CSS passé via css_click_selector n'a correspondu à aucun élément. | Ajoutez un repli au sélecteur (#start-button,body) pour que le clic atteigne tout de même un élément connu. Voir les notes sur css_click_selector pour le modèle complet. |
599 | Erreur interne Crawlbase | Réessayez. Si une requête rencontre cette erreur de manière constante, contactez le support avec le rid. |
La référence complète HTTP + pc_status se trouve dans Codes de statut ; Gestion des erreurs couvre la boucle recommandée de retry-with-backoff et les helpers de SDK qui l'implémentent pour vous dans chaque langage.
Exemple d'ancrage. La raison la plus fréquente pour laquelle pc_status diverge de original_status est un CAPTCHA : le site cible renvoie un 200 (la page captcha s'est rendue correctement) mais Crawlbase reconnaît la réponse comme une page d'interstitiel et expose pc_status: 503 afin que vous puissiez la contourner plutôt que de traiter le HTML du captcha comme vos données.
Codes pc_status non standard. Les codes hors de la plage HTTP habituelle - 601, 999 et similaires - sont des marqueurs internes utilisés par l'équipe d'ingénierie Crawlbase. Ils n'apparaissent dans la réponse que pour vous aider à débuguer lorsque vous contactez le support ; vous n'avez pas besoin de les gérer dans votre code applicatif.
Stratégie de tentatives
La version simple : réessayez les erreurs transitoires (5xx) avec un backoff exponentiel jusqu'à un plafond (typiquement 3 à 5 tentatives), ne réessayez pas les erreurs client (4xx : elles ne se corrigeront pas toutes seules), et changez de type de token une seule fois sur le premier 520/525 avant de poursuivre les nouvelles tentatives. Les helpers des SDK implémentent cette boucle avec des valeurs par défaut raisonnables ; pour un client personnalisé, la règle empirique est :
- Première tentative : ~1 s après l'échec
- Deuxième tentative : ~3 s après l'échec
- Troisième tentative : ~10 s après l'échec
- Au-delà : journaliser + alerter ; les échecs persistants signalent généralement un changement côté cible plutôt qu'un problème réseau transitoire
Toutes les nouvelles tentatives contre cette API sont gratuites : seules les réponses réussies (pc_status: 200) sont décomptées de votre quota. Cela rend le backoff agressif peu coûteux ; le seul vrai coût des retries est la latence ajoutée à votre pipeline.
Performance & bonnes pratiques
Quelques modèles reviennent chez les clients qui exploitent cette API à grande échelle. Les adopter d'emblée évite les catégories de tickets de support les plus fréquentes.
- Utilisez le token le moins cher qui fonctionne. Ne basculez pas sur le JavaScript token « au cas où » : les requêtes avec le Normal token sont plus rapides et utilisent moins de concurrence. Passez au JS uniquement lorsque la réponse Normal est vide ou bloquée par un défi.
- Préférez
ajax_waitàpage_wait. Les délais fixes consomment de la concurrence sur chaque requête, même les rapides.ajax_waitretourne dès que la page passe en network-idle : typiquement plus rapide en moyenne et plus lent uniquement sur les pages dont le chargement traîne réellement. - Faites passer les volumes élevés par async + webhook. Le mode synchrone est le bon choix par défaut pour un usage ponctuel et interactif. Pour les traitements par lot dépassant quelques centaines d'URLs, le mode async (ou l'Enterprise Crawler) garde votre budget de concurrence libre pour de nouvelles soumissions pendant que les crawls en cours se terminent.
- Réutilisez les sessions pour les flux avec état. Si votre cible nécessite une session connectée ou des cookies de panier, conservez un identifiant de session et transmettez-le sur les requêtes suivantes afin de réutiliser la même IP de sortie et le même cookie jar. Voir Authentication pour le modèle de cookie de session.
- Surveillez l'en-tête
remaining. Réduisez la cadence avant d'atteindre votre plafond de concurrence plutôt que de le découvrir via des 429 : la réponse indique le nombre de slots restants, donc un client en bonne santé temporise de manière proactive au lieu de réagir aux erreurs.

