Distill
POST /v2/distill estrae dati strutturati da uno o più URL per conformarli
a uno schema che fornisci tu. Funziona con un motore a due passaggi: prima un
passaggio CSS gratuito (lo JsonCssExtractionStrategy di Crawl4AI guidato dai
tuoi selettori), poi — solo per i campi che il passaggio CSS ha lasciato vuoti —
un passaggio LLM limitato sul claude-haiku-4-5 di Anthropic. Il campo data
della risposta è garantito tornare esattamente nella forma che hai richiesto. È
la risposta di EnConvert a Firecrawl /extract.
Ecco la chiamata utile più piccola. Invia un URL e uno schema flat
{field: description}, e ottieni indietro i campi estratti:
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 risposta riporta un risultato per URL, i data estratti e quale tier
li ha prodotti:
{
"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": []
}
Vale la pena segnalarlo subito.
/v2/distillè un endpoint V2 con un suo contatore di quota,distill_operations, dove una operazione equivale a un URL distillato. I piani di conversione V1 non includono operazioni di distill — ti serve un piano che includa V2 per chiamarlo. Distill fattura solo gli URL che si completano, quindi un fallimento di render di EnConvert non ti costa mai un'unità. Vedi la panoramica della web intelligence V2 per capire come i contatori si relazionano, e la pagina dei prezzi per le allocazioni per tier.
Endpoints
| Metodo | Path | Scopo |
|---|---|---|
POST |
/v2/distill |
Distilla una lista esplicita di URL, oppure scopre prima gli URL di un sito e ne distilla ciascuno, contro un unico schema. |
Content-Type: application/json.
A differenza di perceive, distill è un singolo endpoint sincrono — non c'è un GET separato per il re-fetch né un percorso batch asincrono. Ogni URL viene renderizzato in sequenza attraverso il singleton Chrome headless condiviso e l'intero set di risultati torna in un'unica risposta.
Authentication
Autenticati con una chiave privata nell'header X-API-Key per le chiamate
server-to-server. È il percorso usato dagli esempi qui sotto.
X-API-Key: sk_live_your_private_key
Funzionano anche le chiavi pubbliche con un token bearer JWT, usando lo stesso
flusso di ogni altro endpoint — genera un token con la tua chiave pk_, poi
invialo come Authorization: Bearer <token>. Il flusso completo, incluso il
domain locking e il refresh del token, è ne
la guida all'autenticazione.
Ogni chiave API porta con sé un'allowlist di endpoint consentiti. Se /v2/distill
non è nella lista della chiave, la richiesta viene rifiutata con 403.
Come funziona distill
Una richiesta distilla una lista di URL contro un unico schema. Il flusso è lo stesso per ogni URL:
- Risolvi la lista di URL. Con
urls, la lista è esattamente quella che hai inviato (deduplicata, con l'ordine preservato). Condiscover_from, distill esegue prima discover sull'URL seed — analizzando la sitemap, facendo crawling, o entrambi — poi distilla gli URL scoperti fino amax_pages. - Render. Ogni URL viene renderizzato una volta in Chrome headless attraverso
la stessa pipeline di cattura che alimenta perceive. Il
render viene sottoposto a screening SSRF e, se
respect_robots=true, verificato contro ilrobots.txtdel sito. Nessun artefatto viene caricato in storage — distill ha bisogno solo del DOM renderizzato. - Passaggio 1 — CSS (gratuito). Se hai fornito un
css_schema, l'estrattore CSS gira sull'HTML renderizzato e riempie ogni campo indirizzabile via selettore a costo LLM zero. Questo passaggio è limitato a 10 secondi; in caso di timeout l'URL prosegue verso il passaggio LLM con un warning. - Passaggio 2 — LLM (limitato, solo quando serve). Distill raccoglie i campi
dello schema che il passaggio CSS ha lasciato mancanti o vuoti e fa escalation
solo di quei campi verso
claude-haiku-4-5, sotto rigidi tetti di budget per chiamata e per periodo. Se il tuo piano non ha un tier LLM, la pagina è stata segnalata come bloccata, o si raggiunge un tetto di budget, il passaggio LLM viene saltato e i campi mancanti tornano comenullcon un warning. - Normalizza. Il risultato unito viene rimodellato esattamente sulle chiavi
del tuo schema — gli scalari mancanti diventano
null, gli array mancanti diventano[], e qualsiasi chiave extra viene scartata. La garanzia sulla forma regge a prescindere da ciò che hanno prodotto il CSS o l'LLM.
Il contatore di quota si incrementa di uno solo dopo che un URL si completa. I
fallimenti di render (rifiuto SSRF, blocco robots, un render andato in crash)
producono una riga di risultato failed e non costano quota.
Parametri della richiesta
Devi fornire esattamente uno tra urls o discover_from, più uno schema.
Inviare entrambi, o nessuno, è un 422.
Sorgente — URL espliciti
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
urls |
string[] |
— | URL espliciti da distillare. Ciascuno deve iniziare con http:// o https:// ed essere al massimo di 2.048 caratteri. Massimo 50 URL per richiesta (MAX_DISTILL_URLS). Mutuamente esclusivo con discover_from. |
Sorgente — discover poi distill
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
discover_from |
object |
— | Scopri prima gli URL di un sito, poi distilla ciascuno. Mutuamente esclusivo con urls. Richiede il flag di piano discover_enabled (altrimenti 402). |
discover_from.url |
string |
— | URL seed. Deve iniziare con http:// o https://. Massimo 2.048 caratteri. |
discover_from.mode |
string |
hybrid |
sitemap, crawl, o hybrid. Le stesse modalità de l'endpoint discover. |
discover_from.max_pages |
integer |
10 |
Tetto sugli URL scoperti e distillati. 1–50 — ciascuno è un render completo, quindi è limitato da MAX_DISTILL_URLS. |
Schema (obbligatorio)
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
schema |
object |
— | La forma dell'output, inviata sotto la chiave JSON schema. O un oggetto JSON-Schema ({"type": "object", "properties": {...}}) o una mappa flat e tollerante {field: description}. Massimo 200 proprietà di primo livello. Il campo data della risposta è garantito conforme a questa forma. Uno schema strutturalmente invalido è un 422. Obbligatorio. |
Lo schema è il contratto. Se invii un oggetto JSON-Schema, distill ne legge le
properties; se invii una mappa flat, ogni chiave dà il nome a un campo e ogni
valore è la descrizione passata all'LLM. In entrambi i casi, data torna con
esattamente le chiavi di primo livello dello schema.
CSS schema (opzionale)
Fornisci un css_schema per rispondere ai campi gratuitamente prima di qualsiasi
chiamata LLM. Senza di esso, ogni campo cade direttamente al passaggio LLM.
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
css_schema.baseSelector |
string |
— | Selettore CSS per il contenitore ripetuto; un record viene estratto per ogni corrispondenza. 1–1.024 caratteri. Obbligatorio quando css_schema è presente. |
css_schema.fields |
CssField[] |
— | 1–128 definizioni di campo lette da ciascun contenitore. Obbligatorio. |
css_schema.name |
string |
"distill" |
Etichetta opzionale per lo schema. Massimo 128 caratteri. |
css_schema.target_field |
string |
dedotto | Quale proprietà di output di primo livello riempiono i record CSS — una proprietà array riceve la lista completa di record, una proprietà scalare/oggetto riceve il primo record. Se omesso, distill lo deduce se lo schema ha esattamente una proprietà array. Massimo 128 caratteri. |
Ogni voce in fields è un CssField:
| Campo | Tipo | Default | Descrizione |
|---|---|---|---|
name |
string |
— | Chiave di output per questo campo. 1–128 caratteri. Obbligatorio. |
type |
string |
— | Uno tra text, attribute, html, regex, nested, list, nested_list. Obbligatorio. |
selector |
string |
null |
Sotto-selettore CSS. Opzionale per i tipi foglia (text/attribute/html/regex); obbligatorio per nested/list/nested_list. Massimo 1.024 caratteri. |
attribute |
string |
null |
Nome dell'attributo da leggere. Obbligatorio quando type è attribute. Massimo 128 caratteri. |
pattern |
string |
null |
Pattern regex. Obbligatorio quando type è regex. Compilato all'edge; un pattern con gruppi annidati e ri-quantificati (una forma ReDoS come (a+)+) viene rifiutato con 422. Massimo 1.024 caratteri. |
default |
any | null |
Valore quando il selettore non corrisponde a nulla. |
transform |
string |
null |
Uno tra lowercase, uppercase, strip. |
fields |
CssField[] |
null |
Campi figli, per nested/list/nested_list. Massimo 64 figli; profondità totale di annidamento massimo 5. |
Vale la pena segnalare: il tipo di campo computed di Crawl4AI deliberatamente
non è accettato. La sua forma a espressione esegue eval sull'input del chiamante,
e la sua forma callable non può attraversare un confine JSON, quindi distill
enumera solo i sette tipi sicuri qui sopra.
Manopole di render
Distill renderizza solo il DOM, quindi espone un piccolo sottoinsieme delle opzioni di render di perceive.
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
wait_for |
string |
null |
Attendi dopo la navigazione un selettore CSS o un'espressione JS ("js:window.dataReady === true"). Massimo 1.024 caratteri. |
wait_timeout_ms |
integer |
30000 |
Quanto a lungo wait_for può attendere, in millisecondi. 0–60.000. |
headers |
object |
null |
Header di richiesta personalizzati per il render. |
cookies |
array |
null |
Cookie da iniettare prima della navigazione. |
respect_robots |
boolean |
false |
Quando true, un URL non consentito dal robots.txt del sito viene rifiutato e il risultato di quell'URL viene marcato failed. |
Response
POST /v2/distill restituisce un DistillResponse:
| Campo | Tipo | Descrizione |
|---|---|---|
operation_id |
string |
ID opaco (dst_...). Citalo al supporto. |
total |
integer |
Numero di URL processati (righe in results). |
completed |
integer |
URL che hanno renderizzato e prodotto un oggetto data. |
failed |
integer |
URL il cui render è stato rifiutato o è andato in crash. |
results |
object[] |
Un DistillItemResult per URL — vedi sotto. |
total_cost_cents |
number |
Somma del costo LLM per URL sull'intera richiesta, in centesimi. |
warnings |
string[] |
Note a livello di richiesta (es. quota esaurita a metà lista, errori del crawl di discover). |
Ogni voce in results è un DistillItemResult:
| Campo | Tipo | Descrizione |
|---|---|---|
url |
string |
L'URL che hai inviato (o scoperto). |
url_final |
string |
L'URL dopo i redirect. Omesso su una riga failed. |
status |
string |
completed o failed. |
data |
object |
I dati estratti, normalizzati esattamente sulle chiavi del tuo schema. null su una riga failed. |
extraction_tier |
string |
css (solo CSS), llm (solo LLM), mixed (hanno contribuito entrambi), o none (nulla trovato). |
fields_from_css |
integer |
Conteggio dei campi riempiti dal passaggio CSS. |
fields_from_llm |
integer |
Conteggio dei campi riempiti dal passaggio LLM. |
render_quality |
number |
0.0–1.0. Punteggi bassi segnalano sfide anti-bot o login wall. |
tokens |
object |
Token LLM usati {input, output}. Zero a meno che il passaggio LLM sia girato. |
cost_cents |
number |
Costo LLM in centesimi per questo URL. Zero a meno che il passaggio LLM sia girato. |
error |
string |
Impostato solo quando status è failed. Un messaggio generico — il dettaglio interno del render resta lato server. |
warnings |
string[] |
Note per URL: un timeout CSS, un passaggio LLM saltato, un tetto di budget raggiunto. |
Per essere chiari: la risposta non porta URL di download firmati né artefatti
memorizzati. Distill restituisce i data strutturati inline e nient'altro — se
vuoi anche il Markdown della pagina, l'HTML, uno screenshot o un PDF, è per
quello che esiste perceive.
Il modello di costo a due passaggi
Il passaggio CSS è gratuito. Il passaggio LLM costa, quindi distill lo innesca nel modo più ristretto possibile e lo limita da diverse direzioni.
Fa escalation solo dei campi mancanti. Dopo il passaggio CSS, distill calcola
quali campi dello schema sono ancora vuoti — uno scalare tornato null/"", un
array tornato vuoto, o un array i cui elementi mancano di un sotto-campo dichiarato.
Solo i nomi di quei campi entrano in uno schema ridotto per la chiamata LLM, il
che mantiene minimi il prompt e il costo.
Salta del tutto il passaggio LLM quando vale una qualsiasi di queste condizioni, restituendo il risultato solo-CSS con un warning invece di spendere troppo:
- Il tuo piano non ha un tier LLM (
llm_extraction_enabledpiù unagent_model_tierdiverso danone). - Il
render_qualitydella pagina l'ha segnalata come bloccata da protezione anti-bot. - Il budget LLM per richiesta di questa chiamata è raggiunto, o il tetto di budget per periodo è raggiunto.
I tetti di budget sono a strati:
| Tetto | Valore | Ambito |
|---|---|---|
| Per chiamata | $0.05 (PER_REQUEST_CAP_CENTS) |
Costo proiettato nel caso peggiore di una chiamata LLM. Sopra → saltato prima di qualsiasi I/O di rete. |
| Per richiesta | $0.50 (_REQUEST_LLM_BUDGET_CENTS) |
Spesa LLM totale su tutti gli URL in una chiamata /v2/distill. Gli URL rimanenti tornano solo-CSS. |
| Escalation per richiesta | 50 (_MAX_LLM_ESCALATIONS) |
Al massimo una chiamata LLM per URL, con tetto rigido. |
| Per periodo | $5 di default, $20 su Pro e superiori | ch_usage_periods.llm_cost_cents per il tuo periodo di fatturazione (usage.reserve_llm_budget). |
Nota. Il budget per periodo viene riservato atomicamente prima della chiamata e regolato sul costo reale dopo, così che chiamate concorrenti non possano collettivamente sforare il tetto. Un progetto senza una riga di periodo di utilizzo attivo fallisce in chiusura — una spesa che EnConvert non può contabilizzare è una spesa che non fa. Quando un tetto è raggiunto, i campi interessati tornano come
nullcon un warning; la richiesta riesce comunque.
Quando il passaggio LLM gira, extraction_tier riporta llm o mixed, e
tokens e cost_cents riportano quanto è costato. Quando non gira, entrambi
sono zero.
Mappare i record CSS sul tuo schema
Il caso comune è una pagina di listing: una riga ripetuta, e uno schema di output
con una proprietà array che contiene le righe. Dai a distill un css_schema il
cui baseSelector corrisponde alla riga e i cui fields leggono le colonne, e
riempie l'array gratuitamente:
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"}
]
}
}'
Come i record si depositano sullo schema:
target_fieldimpostato su una proprietà array → quella proprietà riceve la lista completa di record.target_fieldimpostato su una proprietà scalare/oggetto → riceve il primo record.target_fieldomesso, lo schema ha esattamente una proprietà array → distill la deduce e riempie quella proprietà.- Altrimenti → il primo record viene trattato come un singolo oggetto flat e le sue chiavi corrispondenti vengono sollevate al primo livello.
Se il CSS riempie l'array ma ad alcuni elementi manca un sotto-campo dichiarato —
diciamo che sku è assente su metà delle card — distill fa escalation di
products al passaggio LLM per colmare le lacune. È questo il fattore distintivo
del modello a due passaggi rispetto a uno scraper semplice: strutturato dove può,
supportato dal modello dove deve.
Esempi di codice
curl — schema flat, solo-LLM
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 — discover poi distill
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);
Plan gating
Le operazioni di distill sono misurate sul loro contatore distill_operations,
separato dalle conversioni V1 e dal contatore di perceive.
Una operazione equivale a un URL distillato, e vengono contati solo gli URL
che si completano. I piani di conversione V1 non includono quota di distill — ti
serve un piano che includa V2.
Il passaggio LLM guidato dallo schema (claude-haiku-4-5) è un ulteriore gate di
piano: gira solo quando il tuo piano porta llm_extraction_enabled e un agent
model tier diverso da none. Senza di esso, distill restituisce risultati
solo-CSS e annota i campi saltati in warnings. Il budget LLM per periodo è di
$5 sul tier di default e $20 su Pro e superiori.
Usare discover_from richiede inoltre il flag di piano discover_enabled —
altrimenti la richiesta viene rifiutata con 402, così che un piano solo-distill
non possa raggiungere discover gratuitamente instradando attraverso qui.
Le allocazioni di distill_operations per tier e i piani che includono il tier
LLM si trovano su la pagina dei prezzi.
Risposte di errore
| Stato | Condizione |
|---|---|
400 Bad Request |
Un URL (o il seed di discover_from) si risolve in un indirizzo privato, loopback o link-local, non ha hostname, o altrimenti non supera lo screen SSRF. Sollevato per URL durante il render. |
401 Unauthorized |
Chiave API / token JWT mancante o invalido. |
402 Payment Required |
Distill non è sul tuo piano attuale, la tua quota mensile di distill_operations è esaurita, o discover_from è stato inviato senza il flag di piano discover_enabled. |
403 Forbidden |
/v2/distill non è negli endpoint consentiti della chiave API. |
422 Unprocessable Entity |
Nessuno schema, entrambi o nessuno tra urls/discover_from, uno schema strutturalmente invalido, oltre 200 proprietà di schema, un CssField invalido (mancanza di attribute/pattern/fields per il suo tipo, una regex non compilabile o prona al ReDoS), o annidamento di campi CSS più profondo di 5. |
500 Internal Server Error |
L'orchestrazione è fallita in modo inatteso. Il messaggio include l'operation_id da citare al supporto. |
Alcune note sugli stati che vale la pena tenere chiare: un singolo URL il cui
render è rifiutato per SSRF emerge come 400 solo quando è il seed di una
richiesta discover_from; per una lista urls esplicita, un rifiuto di render
per singolo URL diventa una riga di risultato failed invece di far fallire
l'intera richiesta. Un tetto di budget LLM non è mai un errore — degrada a un
risultato solo-CSS con un warning. Il riferimento completo dei codici di stato è
ne la guida ai codici di errore.
Limiti
| Limite | Valore |
|---|---|
URL per richiesta (urls) |
50 (MAX_DISTILL_URLS) |
discover_from.max_pages |
1–50 |
| Lunghezza URL | 2.048 caratteri |
| Proprietà di primo livello dello schema | 200 (MAX_SCHEMA_PROPERTIES) |
css_schema.fields |
1–128 |
Figli di CssField.fields |
64 |
| Profondità di annidamento dei campi CSS | 5 (MAX_CSS_FIELD_DEPTH) |
Lunghezza di wait_for |
1.024 caratteri |
wait_timeout_ms |
0–60.000 ms |
| Timeout del passaggio CSS | 10 secondi per URL |
| Tetto LLM per chiamata | $0.05 |
| Tetto LLM per richiesta | $0.50 |
| Escalation LLM per richiesta | 50 |
| Tetto LLM per periodo | $5 di default / $20 Pro+ |
distill_operations mensili |
Dipendente dal piano — vedi prezzi |