---
seo_title: Distill (fase 1): estrarre dati strutturati | EnConvert
meta_desc: Beta privata, fase 1: estrazione strutturata guidata da schema da qualsiasi URL. Prima una passata CSS gratuita, poi un fallback LLM, nella forma JSON che chiedi.
keywords: estrarre dati strutturati da un sito web api, web scraping con schema json api, api di web scraping con llm, estrazione dati con selettori css api, alternativa a firecrawl extract, api per estrarre dati prodotto da un sito, convertire sito web in json api, estrazione dati strutturati dal web
---

# API per estrarre dati strutturati da siti web

<div class="alert alert-warning">
<strong>Beta privata.</strong> 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 <a href="/it/docs/coming-soon">In arrivo</a>, e ogni rilascio viene annunciato nel <a href="/it/changelog">changelog</a>.
</div>

`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:

```bash
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:

```json
{
    "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](/it/docs/endpoints/perceive.md), 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.

```http
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](/it/docs/authentication.md).

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](/it/docs/coming-soon/discover.md)
   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](/it/docs/endpoints/perceive.md). 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](/it/docs/coming-soon/discover.md). |
| `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](/it/docs/endpoints/perceive.md).

| 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](/it/docs/endpoints/perceive.md).

---

## 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:

```bash
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

```bash
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

```bash
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

```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

```javascript
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](/it/docs/reference/errors.md).

---

## 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](/it/pricing.md) |

---

## 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`.
