API per estrarre dati strutturati da siti web#
POST /v2/distill è un'API per estrarre dati strutturati dai siti web:
estrae campi da uno o più URL per farli corrispondere a uno schema che
fornisci tu, cioè un oggetto JSON-Schema oppure una mappa piatta
{field: description}. Funziona con un motore a due passate: prima una
passata CSS gratuita (la JsonCssExtractionStrategy di Crawl4AI, guidata
dai tuoi selettori), poi una passata assistita da LLM, limitata, per i
campi che la passata CSS ha lasciato vuoti. Il campo data della
risposta è garantito tornare esattamente
nella forma richiesta. Sarà la risposta di EnConvert a /extract di
Firecrawl.
Ecco la chiamata utile più semplice. Invia un URL e uno schema piatto
{field: description} e ricevi indietro i campi estratti:
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_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 contiene un risultato per URL, i dati estratti (data) e
quale livello 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": []
}
Endpoint#
| Metodo | Percorso | Scopo |
|---|---|---|
POST |
/v2/distill |
Distilla un elenco esplicito di URL, oppure scopri prima gli URL di un sito e distilla ciascuno, rispetto a uno schema. |
Content-Type: application/json.
A differenza di perceive, distill è un endpoint sincrono singolo: non esiste un percorso separato di ri-fetch GET o di batch asincrono. Ogni URL viene renderizzato in sequenza tramite il singleton condiviso di Chrome headless e l'intero set di risultati torna in un'unica risposta.
Autenticazione#
Autenticati con una chiave privata nell'header X-API-Key per le
chiamate server-to-server. Questo è il percorso usato negli esempi
seguenti.
X-API-Key: sk_your_private_key
Funzionano anche le chiavi pubbliche con un token bearer JWT, seguendo lo
stesso flusso di ogni altro endpoint: genera un token con la tua chiave
pk_, poi invialo come Authorization: Bearer <token>. Il flusso
completo, compresi il domain locking e il refresh del token, si trova
nella guida all'autenticazione.
Ogni chiave API porta con sé un'allowlist di endpoint consentiti. Se
/v2/distill non è nell'elenco della chiave, la richiesta viene
rifiutata con 403.
Come funziona distill#
Una singola richiesta distilla un elenco di URL rispetto a uno schema. Il flusso è lo stesso per ogni URL:
- Risolvi l'elenco di URL. Con
urls, l'elenco è esattamente quello che hai inviato (deduplicato, ordine preservato). Condiscover_from, distill esegue prima discover sull'URL seed (analizzando la sitemap, effettuando il crawling, o entrambi), poi distilla gli URL scoperti fino amax_pages. - Rendering. Ogni URL viene renderizzato una volta in Chrome headless
attraverso la stessa pipeline di acquisizione che alimenta
perceive. Il rendering viene controllato per
SSRF e, se
respect_robots=true, verificato rispetto alrobots.txtdel sito. Nessun artefatto viene caricato nello storage, perché distill ha bisogno solo del DOM renderizzato. - Passata 1: CSS (gratuita). Se hai fornito un
css_schema, l'estrattore CSS viene eseguito sull'HTML renderizzato e riempie ogni campo indirizzabile da selettore a costo LLM zero. Questa passata è limitata a 10 secondi; allo scadere del timeout, l'URL passa alla passata LLM con un avviso. - Passata 2: LLM (limitata, solo quando serve). Distill raccoglie i
campi dello schema che la passata CSS ha lasciato mancanti o vuoti e
inoltra in escalation solo quei campi al livello LLM, entro
rigidi limiti di budget per chiamata e per periodo. Se il tuo piano
non include un livello LLM, la pagina è stata segnalata come bloccata,
oppure viene raggiunto un limite di budget, la passata LLM viene
saltata e i campi mancanti tornano come
nullcon un avviso. - Normalizzazione. Il risultato unito viene rimodellato esattamente
sulle chiavi del tuo schema: gli scalari mancanti diventano
null, gli array mancanti diventano[], e le chiavi extra vengono scartate. La garanzia sulla forma vale indipendentemente da cosa abbiano prodotto CSS o l'LLM.
Viene addebitata una op per URL, e solo dopo che quell'URL si
completa. I fallimenti di rendering (rifiuto SSRF, blocco robots, un
rendering fallito) producono una riga di risultato failed e non
costano ops.
Parametri della richiesta#
Devi fornire esattamente uno tra urls o discover_from, più uno
schema. Inviarli entrambi, o nessuno dei due, produce un 422.
Sorgente: URL espliciti#
| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
urls |
string[] |
-- | URL espliciti da distillare. Ognuno deve iniziare con http:// o https:// ed essere lungo al massimo 2,048 caratteri. Massimo 50 URL per richiesta (MAX_DISTILL_URLS). Mutuamente esclusivo con discover_from. |
Sorgente: prima scopri, poi distilla#
| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
discover_from |
object |
-- | Scopre 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à dell'endpoint discover. |
discover_from.max_pages |
integer |
10 |
Limite sugli URL scoperti e distillati, da 1 a 50. Ognuno è un rendering completo, quindi è vincolato da MAX_DISTILL_URLS. |
Schema o prompt#
Fornisci o uno schema (la forma dell'output che vuoi) o un
prompt (una descrizione in linguaggio naturale di cosa estrarre).
Esattamente uno dei due è obbligatorio; se li invii entrambi, prevale
schema.
| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
schema |
object |
null |
La forma dell'output, inviata sotto la chiave JSON schema. Un oggetto JSON-Schema ({"type": "object", "properties": {...}}) oppure una mappa piatta e permissiva {field: description}. Massimo 200 proprietà di primo livello. Il campo data della risposta è garantito corrispondere a questa forma. Uno schema strutturalmente non valido produce un 422. |
prompt |
string |
null |
Una descrizione in linguaggio naturale di cosa estrarre. Quando viene fornito senza uno schema, distill sintetizza da esso lo schema di estrazione (un singolo modello) e poi esegue il normale motore a due passate. Massimo 2,000 caratteri. |
Lo schema è il contratto. Se invii un oggetto JSON-Schema, distill legge
le sue properties; se invii una mappa piatta, ogni chiave nomina 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.
Modalità solo-prompt. Se invii un prompt invece di uno schema,
distill sintetizza prima uno schema di campi dal tuo prompt, poi estrae
rispetto ad esso. I campi sintetizzati vengono riportati nella risposta
come synthesized_schema. La modalità solo-prompt usa il livello di
estrazione LLM e richiede un piano che lo includa; senza di esso, distill
restituisce un avviso chiaro invece di tirare a indovinare.
Schema CSS (opzionale)#
Fornisci un css_schema per rispondere ai campi gratuitamente prima di
qualsiasi chiamata LLM. Senza di esso, ogni campo va direttamente alla
passata LLM.
| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
css_schema.baseSelector |
string |
-- | Selettore CSS per il contenitore ripetuto; per ogni corrispondenza viene estratto un record. 1–1,024 caratteri. Obbligatorio quando è presente css_schema. |
css_schema.fields |
CssField[] |
-- | 1–128 definizioni di campo lette da ogni contenitore. Obbligatorio. |
css_schema.name |
string |
"distill" |
Etichetta opzionale per lo schema. Massimo 128 caratteri. |
css_schema.target_field |
string |
inferito | Quale proprietà di output di primo livello riempiono i record CSS: una proprietà array riceve l'intero elenco di record, una proprietà scalare/oggetto riceve il primo record. Se omesso, distill lo inferisce se lo schema ha esattamente una proprietà array. Massimo 128 caratteri. |
Ogni voce in fields è un CssField:
| Campo | Tipo | Predefinito | 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 al margine (edge); un pattern con gruppi ri-quantificati annidati (una forma ReDoS come (a+)+) viene rifiutato con 422. Massimo 1,024 caratteri. |
default |
any | null |
Valore quando il selettore non trova corrispondenze. |
transform |
string |
null |
Uno tra lowercase, uppercase, strip. |
fields |
CssField[] |
null |
Campi figli, per nested/list/nested_list. Massimo 64 figli; profondità di annidamento totale massima 5. |
Da segnalare: il tipo di campo computed di Crawl4AI non è
deliberatamente 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 sopra
elencati.
Opzioni di rendering#
Distill renderizza solo il DOM, quindi espone un piccolo sottoinsieme delle opzioni di rendering di perceive.
| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
wait_for |
string |
null |
Attende dopo la navigazione un selettore CSS o un'espressione JS ("js:window.dataReady === true"). Massimo 1,024 caratteri. |
wait_timeout_ms |
integer |
30000 |
Per quanto tempo wait_for può attendere, in millisecondi. 0–60,000. |
headers |
object |
null |
Header di richiesta personalizzati per il rendering. |
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 come failed. |
Risposta#
POST /v2/distill restituisce una DistillResponse:
| Campo | Tipo | Descrizione |
|---|---|---|
operation_id |
string |
ID opaco (dst_...). Citalo quando contatti il supporto. |
total |
integer |
Numero di URL elaborati (righe in results). |
completed |
integer |
URL che sono stati renderizzati e hanno prodotto un oggetto data. |
failed |
integer |
URL il cui rendering è stato rifiutato o è andato in crash. |
results |
object[] |
Un DistillItemResult per URL, descritto sotto. |
total_cost_cents |
number |
Somma del costo LLM per URL nell'intera richiesta, in centesimi. |
synthesized_schema |
object |
Presente solo in modalità solo-prompt: lo schema sintetizzato dal tuo prompt e usato per l'estrazione. |
warnings |
string[] |
Note a livello di richiesta (es. quota di ops esaurita a metà elenco, errori di crawling di discover). |
Ogni voce in results è un DistillItemResult:
| Campo | Tipo | Descrizione |
|---|---|---|
url |
string |
L'URL che hai inviato (o che è stato 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 (entrambi hanno contribuito), o none (nulla trovato). |
fields_from_css |
integer |
Conteggio dei campi riempiti dalla passata CSS. |
fields_from_llm |
integer |
Conteggio dei campi riempiti dalla passata LLM. |
render_quality |
number |
0.0–1.0. Punteggi bassi segnalano challenge anti-bot o login wall. |
tokens |
object |
Token LLM usati come {input, output}. Zero a meno che la passata LLM non sia stata eseguita. |
cost_cents |
number |
Costo LLM in centesimi per questo URL. Zero a meno che la passata LLM non sia stata eseguita. |
error |
string |
Impostato solo quando status è failed. Un messaggio generico, perché i dettagli interni del rendering restano lato server. |
warnings |
string[] |
Note per URL: un timeout CSS, una passata LLM saltata, un limite di budget raggiunto. |
Per essere diretti: la risposta non contiene URL di download firmati né
artefatti salvati. Distill restituisce i data strutturati inline e
nient'altro. Se vuoi anche il Markdown della pagina, l'HTML, uno
screenshot o un PDF, è a questo che serve perceive.
Il modello di costo a due passate#
La passata CSS è gratuita. La passata LLM costa denaro, quindi distill la attiva nel modo più mirato possibile e la limita da diverse direzioni.
Fa l'escalation solo dei campi mancanti. Dopo la passata 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 quei nomi di campo entrano in uno
schema ridotto per la chiamata LLM, il che mantiene minimi il prompt e
il costo.
Salta del tutto la passata LLM quando una di queste condizioni si verifica, restituendo il risultato solo-CSS con un avviso invece di spendere troppo:
- Il tuo piano non ha un livello LLM (
llm_extraction_enabledinsieme a unagent_model_tierdiverso danone). - Il
render_qualitydella pagina l'ha segnalata come bloccata da protezione anti-bot. - Viene raggiunto il budget LLM per richiesta per questa chiamata, oppure viene raggiunto il limite di budget per periodo.
I limiti di budget sono a più livelli:
| Limite | Valore | Ambito |
|---|---|---|
| Per chiamata | $0.05 (PER_REQUEST_CAP_CENTS) |
Costo massimo previsto per una singola chiamata LLM. Se superato → saltata 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, limite rigido. |
| Per periodo | Il tuo saldo mensile di crediti AI: $5 / $15 / $40 concessi al mese su Indie / Studio / Production, i crediti non usati si accumulano | ch_usage_periods.llm_cost_cents contro i crediti concessi del periodo (usage.reserve_llm_budget). |
Nota. Il budget per periodo viene riservato atomicamente prima della chiamata e regolato sul costo reale dopo, così le chiamate concorrenti non possono superare collettivamente il limite. Un progetto senza una riga di periodo di utilizzo attiva fallisce in modo sicuro, perché una spesa che EnConvert non può contabilizzare è una spesa che non effettua. Quando viene raggiunto un limite, i campi interessati tornano come
nullcon un avviso; la richiesta comunque va a buon fine.
Quando la passata LLM viene effettivamente eseguita, extraction_tier
riporta llm o mixed, e tokens e cost_cents riportano quanto è
costata. Quando non viene eseguita, 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 per contenere le righe. Fornisci a
distill un css_schema il cui baseSelector corrisponde alla riga e i
cui fields leggono le colonne, e riempirà l'array gratuitamente:
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_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 atterrano sullo schema:
target_fieldimpostato su una proprietà array → quella proprietà riceve l'intero elenco di record.target_fieldimpostato su una proprietà scalare/oggetto → riceve il primo record.target_fieldomesso, lo schema ha esattamente una proprietà array → distill la inferisce e riempie quella proprietà.- Altrimenti → il primo record viene trattato come un singolo oggetto piatto e le sue chiavi corrispondenti vengono portate al livello superiore.
Se CSS riempie l'array ma ad alcuni elementi manca un sotto-campo
dichiarato (supponiamo che sku sia assente su metà delle card),
distill fa l'escalation di products verso la passata LLM per colmare
le lacune. Questo è ciò che differenzia le due passate da un semplice
scraper: strutturato dove può esserlo, sostenuto da un modello dove è
necessario.
Esempi di codice#
curl: schema piatto, solo LLM#
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_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: prima discover, poi distill#
curl -X POST https://api.enconvert.com/v2/distill \
-H "X-API-Key: sk_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_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_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);
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 comunque fallisce il controllo SSRF. Sollevato per singolo URL durante il rendering. |
401 Unauthorized |
Chiave API / token JWT mancante o non valido. |
402 Payment Required |
Distill non è incluso nel tuo piano attuale, la tua quota mensile di ops è esaurita, oppure discover_from è stato inviato senza il flag di piano discover_enabled. |
403 Forbidden |
/v2/distill non è tra gli endpoint consentiti della chiave API. |
422 Unprocessable Entity |
Nessuno schema, entrambi o nessuno tra urls/discover_from, uno schema strutturalmente non valido, oltre 200 proprietà dello schema, un CssField non valido (manca attribute/pattern/fields per il suo tipo, una regex non compilabile o soggetta a ReDoS), oppure annidamento dei campi CSS più profondo di 5. |
500 Internal Server Error |
L'orchestrazione è fallita in modo imprevisto. Il messaggio include l'operation_id da citare al supporto. |
Alcune note sugli stati da tenere ben distinte: un singolo URL il cui
rendering viene rifiutato per SSRF emerge come 400 solo quando è il
seed di una richiesta discover_from; per un elenco esplicito di
urls, un rifiuto di rendering per singolo URL diventa una riga di
risultato failed invece di far fallire l'intera richiesta. Un limite
di budget LLM non è mai un errore, perché degrada a un risultato
solo-CSS con un avviso. Il riferimento completo ai codici di stato è
nella 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 wait_for |
1,024 caratteri |
wait_timeout_ms |
0–60,000 ms |
| Timeout passata CSS | 10 secondi per URL |
| Limite per chiamata LLM | $0.05 |
| Limite per richiesta LLM | $0.50 |
| Escalation LLM per richiesta | 50 |
| Limite per periodo LLM | Saldo mensile di crediti AI ($5 / $15 / $40 per livello; i crediti non usati si accumulano) |
| Ops mensili (condivise tra tutti gli endpoint, 1 per URL completato) | 500 / 3.000 / 15.000 / 50.000 per livello; vedi i prezzi |
Domande frequenti#
Come estraggo dati strutturati da un sito web con una API REST?#
Invia POST /v2/distill con urls (fino a 50 per richiesta) e uno schema, un oggetto JSON-Schema oppure una mappa piatta {field: description}. Il campo data della risposta torna normalizzato esattamente sulle chiavi di primo livello del tuo schema.
Posso fare scraping di un sito web in uno schema JSON senza scrivere selettori CSS?#
Sì. css_schema è opzionale e senza di esso ogni campo va direttamente alla passata LLM. Fornire un css_schema riempie gratuitamente i campi indirizzabili da selettore e fa l'escalation solo dei campi che la passata CSS ha lasciato vuoti.
Quanto costa la passata di estrazione LLM, e come viene limitata?#
I limiti di budget sono a più livelli: $0.05 per chiamata LLM, $0.50 per richiesta /v2/distill, al massimo 50 escalation per richiesta, e, per periodo, il tuo saldo mensile di crediti AI ($5 / $15 / $40 su Indie / Studio / Production; i crediti non usati si accumulano). L'estrazione LLM consuma crediti, non ops. Raggiungere un limite non fa mai fallire la richiesta: i campi interessati tornano null con un avviso.
Perché alcuni campi sono null nella mia risposta distill?#
Gli scalari mancanti vengono normalizzati a null (e gli array mancanti a []) per preservare la garanzia sulla forma. La passata LLM viene saltata (con un avviso) quando il tuo piano non ha un livello LLM, il render_quality della pagina l'ha segnalata come bloccata, oppure è stato raggiunto un limite di budget.
Posso fare il crawling di un intero sito ed estrarre lo stesso schema da ogni pagina?#
Sì. Invia discover_from con un url seed, una mode (sitemap, crawl, o hybrid), e max_pages (1–50) al posto di urls. Richiede il flag di piano discover_enabled; senza di esso la richiesta viene rifiutata con 402.