API per estrarre dati strutturati da siti web#

Beta privata. Distill è già chiamabile oggi con la tua normale chiave API, su qualsiasi piano compreso Founding, e scala dalla tua quota mensile di ops come ogni altra chiamata. Non è annunciato né disponibile in generale: le forme di richiesta e risposta possono cambiare senza preavviso e non c'è alcun impegno di stabilità o di supporto, quindi non costruirci ancora sopra nulla di critico. La roadmap è su In arrivo, e ogni rilascio viene annunciato nel changelog.

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:

  1. Risolvi l'elenco di URL. Con urls, l'elenco è esattamente quello che hai inviato (deduplicato, ordine preservato). Con discover_from, distill esegue prima discover sull'URL seed (analizzando la sitemap, effettuando il crawling, o entrambi), poi distilla gli URL scoperti fino a max_pages.
  2. 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 al robots.txt del sito. Nessun artefatto viene caricato nello storage, perché distill ha bisogno solo del DOM renderizzato.
  3. 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.
  4. 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 null con un avviso.
  5. 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_enabled insieme a un agent_model_tier diverso da none).
  • Il render_quality della 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 null con 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_field impostato su una proprietà array → quella proprietà riceve l'intero elenco 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 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.