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:

  1. Risolvi la lista di URL. Con urls, la lista è esattamente quella che hai inviato (deduplicata, con l'ordine preservato). Con discover_from, distill esegue prima discover sull'URL seed — analizzando la sitemap, facendo crawling, o entrambi — poi distilla gli URL scoperti fino a max_pages.
  2. 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 il robots.txt del sito. Nessun artefatto viene caricato in storage — distill ha bisogno solo del DOM renderizzato.
  3. 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.
  4. 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 come null con un warning.
  5. 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_enabled più un agent_model_tier diverso da none).
  • Il render_quality della 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 null con 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_field impostato su una proprietà array → quella proprietà riceve la lista completa di record.
  • target_field impostato su una proprietà scalare/oggetto → riceve il primo record.
  • target_field omesso, 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