Distill
POST /v2/distill extrait des données structurées d'une ou plusieurs URL
pour correspondre à un schéma que vous fournissez. Il s'appuie sur un
moteur à deux passes : d'abord une passe CSS gratuite
(le JsonCssExtractionStrategy de Crawl4AI piloté par vos sélecteurs), puis —
uniquement pour les champs laissés vides par la passe CSS — une passe LLM
plafonnée sur le claude-haiku-4-5 d'Anthropic. Le data de la réponse
est garanti de revenir exactement dans la forme que vous avez demandée.
C'est la réponse d'EnConvert au /extract de Firecrawl.
Voici le plus petit appel utile. Envoyez une URL et un schéma plat
{field: description}, et récupérez les champs extraits :
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com/product/widget"],
"schema": {
"name": "the product name",
"price": "the listed price",
"in_stock": "whether it is in stock"
}
}'
La réponse porte un résultat par URL, le data extrait, et quel niveau
l'a produit :
{
"operation_id": "dst_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
"total": 1,
"completed": 1,
"failed": 0,
"results": [
{
"url": "https://example.com/product/widget",
"url_final": "https://example.com/product/widget",
"status": "completed",
"data": {
"name": "Widget Pro",
"price": "$49.00",
"in_stock": "yes"
},
"extraction_tier": "llm",
"fields_from_css": 0,
"fields_from_llm": 3,
"render_quality": 0.91,
"tokens": {"input": 4120, "output": 38},
"cost_cents": 0.45,
"warnings": []
}
],
"total_cost_cents": 0.45,
"warnings": []
}
À signaler d'emblée.
/v2/distillest un endpoint V2 doté de son propre compteur de quota,distill_operations, où une opération équivaut à une URL distillée. Les forfaits de conversion V1 n'incluent aucune opération distill — il vous faut un forfait incluant V2 pour l'appeler. Distill ne facture que les URL qui aboutissent, donc un échec de rendu EnConvert ne vous coûte jamais une unité. Consultez l'aperçu de l'intelligence web V2 pour comprendre comment les compteurs se rattachent les uns aux autres, et la page de tarification pour les allocations par palier.
Endpoints
| Méthode | Chemin | Objet |
|---|---|---|
POST |
/v2/distill |
Distiller une liste d'URL explicite, ou découvrir d'abord les URL d'un site puis distiller chacune, contre un seul schéma. |
Content-Type : application/json.
Contrairement à perceive, distill est un endpoint synchrone unique — il n'y a pas de re-récupération GET distincte ni de chemin de lot asynchrone. Chaque URL est rendue séquentiellement via le singleton Chrome headless partagé et l'ensemble complet des résultats revient dans une seule réponse.
Authentification
Authentifiez-vous avec une clé privée dans l'en-tête X-API-Key pour les
appels serveur à serveur. C'est le chemin utilisé par les exemples
ci-dessous.
X-API-Key: sk_live_your_private_key
Les clés publiques avec un jeton bearer JWT fonctionnent aussi, selon le
même flux que tous les autres endpoints — générez un jeton avec votre clé
pk_, puis envoyez-le sous la forme Authorization: Bearer <token>. Le
flux complet, y compris le verrouillage de domaine et le rafraîchissement
de jeton, figure dans le guide d'authentification.
Chaque clé d'API porte une liste blanche d'endpoints autorisés. Si
/v2/distill ne figure pas sur la liste de la clé, la requête est rejetée
avec un 403.
Comment fonctionne distill
Une requête distille une liste d'URL contre un seul schéma. Le flux est le même pour chaque URL :
- Résoudre la liste d'URL. Avec
urls, la liste est exactement ce que vous avez envoyé (dédoublonnée, ordre préservé). Avecdiscover_from, distill exécute d'abord discover sur l'URL de départ — en analysant le sitemap, en explorant, ou les deux — puis distille les URL découvertes jusqu'àmax_pages. - Rendre. Chaque URL est rendue une fois dans Chrome headless via le
même pipeline de capture qui alimente perceive.
Le rendu est filtré contre le SSRF et, si
respect_robots=true, vérifié contre lerobots.txtdu site. Aucun artefact n'est téléversé vers le stockage — distill n'a besoin que du DOM rendu. - Passe 1 — CSS (gratuite). Si vous avez fourni un
css_schema, l'extracteur CSS s'exécute sur le HTML rendu et remplit tout champ adressable par sélecteur à coût LLM nul. Cette passe est bornée dans le temps à 10 secondes ; en cas de dépassement, l'URL bascule vers la passe LLM avec un avertissement. - Passe 2 — LLM (plafonnée, seulement si nécessaire). Distill
rassemble les champs du schéma laissés manquants ou vides par la passe
CSS et escalade uniquement ces champs vers
claude-haiku-4-5, sous des plafonds de budget stricts par appel et par période. Si votre forfait n'a pas de palier LLM, si la page a été signalée comme bloquée, ou si un plafond de budget est atteint, la passe LLM est ignorée et les champs manquants reviennent ennullavec un avertissement. - Normaliser. Le résultat fusionné est remodelé pour correspondre
exactement aux clés de votre schéma — les scalaires manquants
deviennent
null, les tableaux manquants deviennent[], et toute clé supplémentaire est supprimée. La garantie de forme tient quel que soit ce que CSS ou le LLM a produit.
Le compteur de quota s'incrémente de un uniquement après l'aboutissement
d'une URL. Les échecs de rendu (rejet SSRF, blocage robots, rendu planté)
produisent une ligne de résultat failed et ne coûtent aucun quota.
Paramètres de requête
Vous devez fournir exactement l'un de urls ou discover_from, plus
un schema. Envoyer les deux, ou aucun, est un 422.
Source — URL explicites
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
urls |
string[] |
— | URL explicites à distiller. Chacune doit commencer par http:// ou https:// et faire au plus 2 048 caractères. Max 50 URL par requête (MAX_DISTILL_URLS). Mutuellement exclusif avec discover_from. |
Source — découvrir puis distiller
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
discover_from |
object |
— | Découvrir d'abord les URL d'un site, puis distiller chacune. Mutuellement exclusif avec urls. Nécessite le drapeau de forfait discover_enabled (sinon 402). |
discover_from.url |
string |
— | URL de départ. Doit commencer par http:// ou https://. Max 2 048 caractères. |
discover_from.mode |
string |
hybrid |
sitemap, crawl, ou hybrid. Mêmes modes que l'endpoint discover. |
discover_from.max_pages |
integer |
10 |
Plafond d'URL découvertes et distillées. 1–50 — chacune est un rendu complet, donc bornée par MAX_DISTILL_URLS. |
Schéma (requis)
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
schema |
object |
— | La forme de sortie, envoyée sous la clé JSON schema. Soit un objet JSON-Schema ({"type": "object", "properties": {...}}), soit une carte plate tolérante {field: description}. Max 200 propriétés de premier niveau. Le data de la réponse est garanti de correspondre à cette forme. Un schéma structurellement invalide est un 422. Requis. |
Le schéma est le contrat. Si vous envoyez un objet JSON-Schema, distill
lit ses properties ; si vous envoyez une carte plate, chaque clé nomme un
champ et chaque valeur est la description remise au LLM. Dans les deux cas,
data revient avec exactement les clés de premier niveau du schéma.
Schéma CSS (optionnel)
Fournissez un css_schema pour répondre à des champs gratuitement avant
tout appel LLM. Sans lui, chaque champ bascule directement vers la passe
LLM.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
css_schema.baseSelector |
string |
— | Sélecteur CSS pour le conteneur répétitif ; un enregistrement est extrait par correspondance. 1–1 024 caractères. Requis lorsque css_schema est présent. |
css_schema.fields |
CssField[] |
— | 1–128 définitions de champs lues dans chaque conteneur. Requis. |
css_schema.name |
string |
"distill" |
Libellé optionnel pour le schéma. Max 128 caractères. |
css_schema.target_field |
string |
inféré | Quelle propriété de sortie de premier niveau les enregistrements CSS remplissent — une propriété tableau reçoit la liste complète des enregistrements, une propriété scalaire/objet reçoit le premier enregistrement. Si omis, distill l'infère lorsque le schéma a exactement une propriété tableau. Max 128 caractères. |
Chaque entrée de fields est un CssField :
| Champ | Type | Défaut | Description |
|---|---|---|---|
name |
string |
— | Clé de sortie pour ce champ. 1–128 caractères. Requis. |
type |
string |
— | L'un de text, attribute, html, regex, nested, list, nested_list. Requis. |
selector |
string |
null |
Sous-sélecteur CSS. Optionnel pour les types feuille (text/attribute/html/regex) ; requis pour nested/list/nested_list. Max 1 024 caractères. |
attribute |
string |
null |
Nom de l'attribut à lire. Requis lorsque type vaut attribute. Max 128 caractères. |
pattern |
string |
null |
Motif regex. Requis lorsque type vaut regex. Compilé en périphérie ; un motif avec des groupes re-quantifiés imbriqués (une forme ReDoS comme (a+)+) est rejeté avec 422. Max 1 024 caractères. |
default |
any | null |
Valeur lorsque le sélecteur ne correspond à rien. |
transform |
string |
null |
L'un de lowercase, uppercase, strip. |
fields |
CssField[] |
null |
Champs enfants, pour nested/list/nested_list. Max 64 enfants ; profondeur d'imbrication totale max 5. |
À signaler : le type de champ computed de Crawl4AI n'est délibérément pas
accepté. Sa forme expression exécute eval sur l'entrée de l'appelant, et
sa forme appelable ne peut franchir une frontière JSON, donc distill
n'énumère que les sept types sûrs ci-dessus.
Réglages de rendu
Distill ne rend que le DOM, il expose donc un petit sous-ensemble des options de rendu de perceive.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
wait_for |
string |
null |
Attendre après la navigation un sélecteur CSS ou une expression JS ("js:window.dataReady === true"). Max 1 024 caractères. |
wait_timeout_ms |
integer |
30000 |
Combien de temps wait_for peut attendre, en millisecondes. 0–60 000. |
headers |
object |
null |
En-têtes de requête personnalisés pour le rendu. |
cookies |
array |
null |
Cookies à injecter avant la navigation. |
respect_robots |
boolean |
false |
Lorsque true, une URL interdite par le robots.txt du site est rejetée et le résultat de cette URL est marqué failed. |
Réponse
POST /v2/distill renvoie un DistillResponse :
| Champ | Type | Description |
|---|---|---|
operation_id |
string |
ID opaque (dst_...). Citez-le au support. |
total |
integer |
Nombre d'URL traitées (lignes dans results). |
completed |
integer |
URL qui ont été rendues et ont produit un objet data. |
failed |
integer |
URL dont le rendu a été rejeté ou a planté. |
results |
object[] |
Un DistillItemResult par URL — voir ci-dessous. |
total_cost_cents |
number |
Somme du coût LLM par URL sur l'ensemble de la requête, en cents. |
warnings |
string[] |
Notes au niveau de la requête (p. ex. quota épuisé en milieu de liste, défauts d'exploration discover). |
Chaque entrée de results est un DistillItemResult :
| Champ | Type | Description |
|---|---|---|
url |
string |
L'URL que vous avez envoyée (ou découverte). |
url_final |
string |
L'URL après redirections. Omise sur une ligne failed. |
status |
string |
completed ou failed. |
data |
object |
Les données extraites, normalisées pour correspondre exactement aux clés de votre schéma. null sur une ligne failed. |
extraction_tier |
string |
css (CSS seul), llm (LLM seul), mixed (les deux ont contribué), ou none (rien trouvé). |
fields_from_css |
integer |
Nombre de champs remplis par la passe CSS. |
fields_from_llm |
integer |
Nombre de champs remplis par la passe LLM. |
render_quality |
number |
0.0–1.0. Les scores faibles signalent des défis anti-bot ou des murs de connexion. |
tokens |
object |
{input, output} jetons LLM utilisés. Zéro sauf si la passe LLM s'est exécutée. |
cost_cents |
number |
Coût LLM en cents pour cette URL. Zéro sauf si la passe LLM s'est exécutée. |
error |
string |
Défini uniquement lorsque status vaut failed. Un message générique — le détail interne du rendu reste côté serveur. |
warnings |
string[] |
Notes par URL : un dépassement de délai CSS, une passe LLM ignorée, un plafond de budget atteint. |
Pour être clair : la réponse ne porte aucune URL de téléchargement signée
ni aucun artefact stocké. Distill renvoie le data structuré en ligne et
rien d'autre — si vous voulez aussi le Markdown de la page, le HTML, une
capture d'écran ou un PDF, c'est à cela que sert
perceive.
Le modèle de coût à deux passes
La passe CSS est gratuite. La passe LLM coûte de l'argent, donc distill la déclenche aussi étroitement que possible et la plafonne sous plusieurs angles.
Elle escalade uniquement les champs manquants. Après la passe CSS,
distill calcule quels champs du schéma sont encore vides — un scalaire
revenu en null/"", un tableau revenu vide, ou un tableau dont les
éléments manquent d'un sous-champ déclaré. Seuls ces noms de champs entrent
dans un schéma réduit pour l'appel LLM, ce qui garde le prompt et le coût
minimaux.
Elle ignore complètement la passe LLM lorsque l'une de ces conditions tient, renvoyant le résultat CSS seul avec un avertissement plutôt que de trop dépenser :
- Votre forfait n'a pas de palier LLM (
llm_extraction_enabledplus unagent_model_tiernon-none). - Le
render_qualityde la page l'a signalée comme bloquée par une protection anti-bot. - Le budget LLM par requête pour cet appel est atteint, ou le plafond de budget par période est atteint.
Les plafonds de budget sont en couches :
| Plafond | Valeur | Portée |
|---|---|---|
| Par appel | 0,05 $ (PER_REQUEST_CAP_CENTS) |
Coût projeté du pire cas d'un appel LLM. Au-delà → ignoré avant toute E/S réseau. |
| Par requête | 0,50 $ (_REQUEST_LLM_BUDGET_CENTS) |
Dépense LLM totale sur toutes les URL d'un appel /v2/distill. Les URL restantes renvoient du CSS seul. |
| Escalades par requête | 50 (_MAX_LLM_ESCALATIONS) |
Au plus un appel LLM par URL, strictement plafonné. |
| Par période | 5 $ par défaut, 20 $ sur Pro et au-dessus | ch_usage_periods.llm_cost_cents pour votre période de facturation (usage.reserve_llm_budget). |
Note. Le budget par période est réservé de manière atomique avant l'appel et réglé au coût réel après, de sorte que des appels concurrents ne peuvent collectivement dépasser le plafond. Un projet sans ligne de période d'usage active échoue en mode fermé — une dépense qu'EnConvert ne peut comptabiliser est une dépense qu'il ne fait pas. Lorsqu'un plafond est atteint, les champs affectés reviennent en
nullavec un avertissement ; la requête réussit tout de même.
Lorsque la passe LLM s'exécute, extraction_tier indique llm ou
mixed, et tokens et cost_cents rapportent ce qu'elle a coûté.
Lorsqu'elle ne s'exécute pas, les deux sont à zéro.
Mapper les enregistrements CSS sur votre schéma
Le cas courant est une page de listing : une ligne répétitive, et un schéma
de sortie avec une propriété tableau pour contenir les lignes. Donnez à
distill un css_schema dont le baseSelector correspond à la ligne et
dont les fields lisent les colonnes, et il remplit le tableau
gratuitement :
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com/products"],
"schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"price": {"type": "string"},
"sku": {"type": "string"}
}
}
}
}
},
"css_schema": {
"baseSelector": ".product-card",
"target_field": "products",
"fields": [
{"name": "name", "type": "text", "selector": ".title"},
{"name": "price", "type": "text", "selector": ".price"},
{"name": "sku", "type": "attribute",
"selector": ".product-card", "attribute": "data-sku"}
]
}
}'
Comment les enregistrements atterrissent sur le schéma :
target_fieldréglé sur une propriété tableau → cette propriété reçoit la liste complète des enregistrements.target_fieldréglé sur une propriété scalaire/objet → elle reçoit le premier enregistrement.target_fieldomis, le schéma a exactement une propriété tableau → distill l'infère et remplit cette propriété.- Sinon → le premier enregistrement est traité comme un seul objet plat et ses clés correspondantes sont remontées au niveau supérieur.
Si CSS remplit le tableau mais que certains éléments manquent d'un
sous-champ déclaré — disons que sku est absent sur la moitié des cartes —
distill escalade products vers la passe LLM pour combler les lacunes.
C'est le différenciateur à deux passes par rapport à un simple scraper :
structuré là où il le peut, soutenu par le modèle là où il le doit.
Exemples de code
curl — schéma plat, LLM seul
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com/article"],
"schema": {
"headline": "the article headline",
"author": "the author name",
"published": "the publish date"
}
}'
curl — découvrir puis distiller
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"discover_from": {
"url": "https://example.com/blog",
"mode": "sitemap",
"max_pages": 25
},
"schema": {
"title": "the post title",
"summary": "a one-line summary"
}
}'
Python
import requests
response = requests.post(
"https://api.enconvert.com/v2/distill",
headers={"X-API-Key": "sk_live_your_private_key"},
json={
"urls": ["https://example.com/product/widget"],
"schema": {
"name": "the product name",
"price": "the listed price",
"in_stock": "whether it is in stock",
},
},
)
response.raise_for_status()
result = response.json()
for item in result["results"]:
if item["status"] == "completed":
print(item["url"], "->", item["data"])
else:
print(item["url"], "FAILED:", item["error"])
print("total cost (cents):", result["total_cost_cents"])
Node.js
const res = await fetch("https://api.enconvert.com/v2/distill", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "sk_live_your_private_key"
},
body: JSON.stringify({
urls: ["https://example.com/product/widget"],
schema: {
name: "the product name",
price: "the listed price",
in_stock: "whether it is in stock"
}
})
});
const result = await res.json();
for (const item of result.results) {
if (item.status === "completed") {
console.log(item.url, "->", item.data);
} else {
console.log(item.url, "FAILED:", item.error);
}
}
console.log("total cost (cents):", result.total_cost_cents);
Restrictions par forfait
Les opérations distill sont mesurées sur leur propre compteur
distill_operations, distinct des conversions V1 et du compteur
perceive. Une opération équivaut à une URL
distillée, et seules les URL qui aboutissent sont comptées. Les forfaits
de conversion V1 n'incluent aucun quota distill — il vous faut un forfait
incluant V2.
La passe LLM pilotée par le schéma (claude-haiku-4-5) est une
restriction de forfait supplémentaire : elle ne s'exécute que lorsque votre
forfait porte llm_extraction_enabled et un palier de modèle d'agent
non-none. Sans cela, distill renvoie des résultats CSS seul et note les
champs ignorés dans warnings. Le budget LLM par période est de 5 $ sur le
palier par défaut et de 20 $ sur Pro et au-dessus.
Utiliser discover_from requiert en plus le drapeau de forfait
discover_enabled — sinon la requête est rejetée avec un 402, de sorte
qu'un forfait distill seul ne peut atteindre discover gratuitement en
passant par ici.
Les allocations distill_operations par palier et les forfaits qui
incluent le palier LLM figurent sur la page de tarification.
Réponses d'erreur
| Statut | Condition |
|---|---|
400 Bad Request |
Une URL (ou l'URL de départ de discover_from) se résout en une adresse privée, de boucle locale ou link-local, n'a pas de nom d'hôte, ou échoue autrement au filtre SSRF. Levé par URL pendant le rendu. |
401 Unauthorized |
Clé d'API / jeton JWT manquant ou invalide. |
402 Payment Required |
Distill n'est pas sur votre forfait actuel, votre quota mensuel distill_operations est épuisé, ou discover_from a été envoyé sans le drapeau de forfait discover_enabled. |
403 Forbidden |
/v2/distill ne figure pas dans les endpoints autorisés de la clé d'API. |
422 Unprocessable Entity |
Pas de schema, les deux ou aucun de urls/discover_from, un schéma structurellement invalide, plus de 200 propriétés de schéma, un CssField invalide (attribute/pattern/fields manquant pour son type, une regex incompilable ou sujette au ReDoS), ou une imbrication de champs CSS plus profonde que 5. |
500 Internal Server Error |
L'orchestration a échoué de manière inattendue. Le message inclut l'operation_id à citer au support. |
Quelques notes de statut à garder claires : une URL unique dont le rendu
est rejeté pour SSRF fait surface un 400 uniquement lorsqu'elle est l'URL
de départ d'une requête discover_from ; pour une liste urls explicite,
un rejet de rendu par URL devient une ligne de résultat failed plutôt que
de faire échouer toute la requête. Un plafond de budget LLM n'est jamais
une erreur — il se dégrade en un résultat CSS seul avec un avertissement.
La référence complète des codes de statut figure dans
le guide des codes d'erreur.
Limites
| Limite | Valeur |
|---|---|
URL par requête (urls) |
50 (MAX_DISTILL_URLS) |
discover_from.max_pages |
1–50 |
| Longueur d'URL | 2 048 caractères |
| Propriétés de schéma de premier niveau | 200 (MAX_SCHEMA_PROPERTIES) |
css_schema.fields |
1–128 |
Enfants CssField.fields |
64 |
| Profondeur d'imbrication de champs CSS | 5 (MAX_CSS_FIELD_DEPTH) |
Longueur de wait_for |
1 024 caractères |
wait_timeout_ms |
0–60 000 ms |
| Délai de la passe CSS | 10 secondes par URL |
| Plafond LLM par appel | 0,05 $ |
| Plafond LLM par requête | 0,50 $ |
| Escalades LLM par requête | 50 |
| Plafond LLM par période | 5 $ par défaut / 20 $ Pro+ |
distill_operations mensuelles |
Dépend du forfait — voir tarification |