---
seo_title: Web Scraping API per Markdown, Screenshot e PDF | EnConvert
meta_desc: POST /v2/perceive renderizza una pagina JavaScript in Chrome headless e restituisce Markdown, screenshot, PDF e dati strutturati in un'unica chiamata API.
keywords: api per fare scraping di pagine web, convertire una pagina web in markdown api, api per screenshot di un sito web, estrarre dati strutturati da una pagina web api, rendering javascript di una pagina per api, leggere una pagina web per llm api, html in markdown api, scraping di più url in batch api
---

# API di Web Scraping per Markdown, Screenshot e Dati Strutturati

`POST /v2/perceive` è l'API di web scraping di EnConvert: renderizza un URL
una sola volta in un browser headless reale (JavaScript eseguito, contenuti
lazy caricati) e restituisce ogni output richiesto da quel singolo render:
Markdown pulito (di default solo il contenuto principale, senza il chrome
del sito), HTML pulito o raw, uno screenshot, un PDF, l'inventario di link
e immagini, e dati strutturati (metadata della pagina, JSON-LD,
intestazioni, tabelle). Gli output file tornano come URL di download
pre-firmati a breve durata, il blocco strutturato inline, e i batch oltre
10 URL vengono eseguiti in modo asincrono dietro un `job_id` da interrogare
con il polling. Un'unica richiesta sostituisce un'intera serie di chiamate
separate: url-to-markdown, url-to-screenshot, url-to-pdf, più il tuo
scraping personalizzato.

Ecco la chiamata utile più semplice. Invia un URL e ricevi Markdown pulito
e i metadata strutturati della pagina:

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown", "structured"]
  }'
```

La risposta include un URL di download pre-firmato per il file Markdown e
il blocco strutturato inline:

```json
{
    "operation_id": "per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "status": "completed",
    "url": "https://example.com/pricing",
    "url_final": "https://example.com/pricing",
    "content_hash": "9f2b8c1a...d4e5",
    "render_quality": 0.93,
    "cache_hit": false,
    "outputs": {
        "markdown": {
            "url": "https://spaces.example.com/...signed...",
            "object_key": "env/files/4127/v2-perceive/per_3f9a..._markdown.md",
            "size_bytes": 8421,
            "content_type": "text/markdown; charset=utf-8",
            "expires_in": 900
        }
    },
    "structured": {
        "metadata": {
            "title": "Pricing",
            "description": "Simple, usage-based pricing."
        },
        "structured_data": [
            {"@type": "Product", "name": "Studio", "offers": {"price": "99"}}
        ]
    },
    "extraction_tier": "heuristic",
    "tokens": {"input": 0, "output": 0},
    "cost_cents": 0.0,
    "duration_ms": 6230,
    "warnings": []
}
```

---

## Endpoint

| Metodo | Percorso | Scopo |
|--------|------|---------|
| `POST` | `/v2/perceive` | Esegue il perceive di un singolo URL e restituisce gli output richiesti. |
| `GET` | `/v2/perceive/{operation_id}` | Recupera di nuovo un'operazione passata con URL di download firmati di fresco. |
| `POST` | `/v2/perceive/batch` | Esegue il perceive di un massimo di 1.000 URL che condividono un unico set di opzioni. |
| `GET` | `/v2/perceive/batch/{job_id}` | Effettua il polling dello stato e dei risultati per-URL di un batch. |
| `DELETE` | `/v2/perceive/batch/{job_id}` | Annulla un batch in esecuzione. |

**Content-Type:** `application/json` su ogni `POST`.

---

## Autenticazione

Autenticati con una chiave privata nell'header `X-API-Key` per le chiamate
server-to-server. Questo è il percorso usato dagli esempi seguenti.

```http
X-API-Key: sk_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, è nella
[guida all'autenticazione](/it/docs/authentication.md).

Ogni chiave API porta con sé un'allowlist di endpoint consentiti. Se
`/v2/perceive` non è nella lista della chiave, la richiesta viene rifiutata
con `403`.

---

## Come funziona perceive

Una richiesta attiva un singolo render del browser tramite un singleton
Chrome headless condiviso, poi materializza ogni output da quel render. Non
paghi mai la stessa pagina due volte in un'unica chiamata.

1. **Render.** La pagina viene recuperata tramite un fallback multi-engine
   automatico: prima una veloce impronta TLS da browser reale, con
   escalation a Chrome headless quando la pagina è bloccata o richiede
   JavaScript, e ancora una volta a un render con hardening stealth quando
   una pagina sembra ancora bloccata da protezioni anti-bot, così più
   pagine del mondo reale tornano con contenuti utilizzabili. Nel browser,
   i banner dei cookie vengono chiusi, la pagina viene scrollata per
   attivare i contenuti lazy, gli header sticky vengono gestiti, e alle
   immagini viene dato il tempo di caricarsi. È la stessa pipeline di
   cattura che alimenta
   [l'endpoint url-to-pdf](/it/docs/endpoints/convert/web-pages/url-to-pdf.md).
2. **Materializza.** Dal DOM renderizzato, perceive costruisce tutto ciò
   che hai elencato in `outputs`: Markdown, HTML pulito/raw, link, immagini,
   uno screenshot, un PDF. Il DOM viene prima normalizzato affinché il
   Markdown rifletta ciò che vede un lettore: i fence di codice mantengono il
   loro linguaggio, i link delle card la loro struttura, e gli elementi di
   interfaccia vengono rimossi con `only_main_content`. Vedi
   [Qualità del Markdown](#qualita-del-markdown).
3. **Estrai.** Se hai richiesto l'output `structured`, perceive esegue un
   passaggio euristico per metadata della pagina, JSON-LD, intestazioni e
   tabelle. Se invii anche uno `schema` e il tuo piano include il livello
   LLM, un passaggio assistito da LLM completa lo schema quando il
   passaggio euristico non basta.
4. **Valuta.** Un punteggio di render-quality (0.0–1.0) distingue un
   render reale da uno fallito. Punteggi sotto 0.40 indicano un render
   fallito: una pagina anti-bot, un login wall, una pagina di errore
   HTTP, un soft 404 o un guscio vuoto. Consulta `deductions` per il
   motivo e `status_code` per lo stato del server di origine.

Gli output binari e testuali (Markdown, HTML, screenshot, PDF, il JSON di
link e immagini) vengono caricati sullo storage e restituiti come **URL
pre-firmati** che scadono dopo 15 minuti. Il blocco `structured` viene
restituito inline nel JSON. Recupera di nuovo un'operazione con
`GET /v2/perceive/{operation_id}` per ottenere un nuovo set di URL firmati.

---

## Parametri della richiesta

La validazione è rigorosa: una chiave della richiesta sconosciuta allo
schema viene rifiutata con `422` indicando il campo interessato. Le chiavi
sconosciute non vengono mai ignorate silenziosamente. Ogni body
`422` include inoltre un array `errors` di primo livello con messaggi
leggibili, accanto alla lista `detail` leggibile dalle macchine.

### Principali

| Parametro | Tipo | Predefinito | Descrizione |
|-----------|------|---------|-------------|
| `url` | `string` | -- | La pagina su cui eseguire il perceive. Deve iniziare con `http://` o `https://`. Massimo 2,048 caratteri. Obbligatorio. |
| `outputs` | `string[]` | `["markdown", "structured"]` | Quali output produrre. Vedi [Output](#outputs). |
| `extract` | `string[]` | `[]` | Quali campi strutturati estrarre quando `structured` è presente in `outputs`. Vedi [Estrazione strutturata](#estrazione-strutturata). |
| `schema` | `object` | `null` | Uno schema JSON che descrive i campi che vuoi estrarre. Attiva il livello di estrazione LLM sui piani che lo includono. |
| `only_main_content` | `boolean` | `true` | Rimuove il chrome del sito (navigazione, header, footer, sidebar, banner dei cookie, nodi nascosti) e gli elementi di interfaccia (pulsanti, barre di tab, widget «Questa pagina ti è stata utile?», etichette destinate ai soli screen reader, breadcrumb) dall'output `markdown` e dall'extract `main_content`, protetto da una guardia di fedeltà: se la rimozione eliminasse troppo contenuto reale, viene restituita la pagina completa e viene aggiunto un warning. Gli URL delle immagini vengono resi come il loro testo alt (la lista completa delle immagini resta disponibile via `outputs: ["images"]`). Imposta `false` per la pagina completa, senza rimuovere nulla. Vedi [Qualità del Markdown](#qualita-del-markdown). |
| `truncate_data_arrays` | `boolean` | non impostato | Comprime lunghe sequenze di letterali numerici (vettori di embedding grezzi, dump di tensori stampati nelle celle di output dei notebook) in un campione iniziale più un conteggio, ad es. `... [truncated 1520 of 1536 values]`. Se non impostato segue `only_main_content`: attivo quando la pagina viene ripulita, inattivo quando hai chiesto la pagina così com'è. Imposta `true` o `false` per controllarlo esplicitamente. |
| `allow_degraded` | `boolean` | `false` | Restituisce il render anche quando è una challenge anti-bot o una pagina di blocco priva di contenuto. Per impostazione predefinita un render simile fallisce con `502` invece di consegnare il testo dell'interstiziale come se fosse la pagina. |
| `direct_download` | `boolean` | `false` | Restituisce i byte dell'artefatto direttamente come body della risposta HTTP invece di una busta JSON. Richiede esattamente un output che produca un artefatto. Solo per richieste a URL singolo, perché l'endpoint batch lo rifiuta con `422`. Vedi [Download diretto](#download-diretto). |
| `cache_mode` | `string` | `"enabled"` | `enabled`, `bypass`, o `refresh`. Vedi [Caching](#caching). |

### Output {: #outputs }

`outputs` accetta qualsiasi combinazione di questi nomi:

| Output | Restituito come | Cosa ottieni |
|--------|-------------|--------------|
| `markdown` | URL firmato | Markdown pulito della pagina. Con `only_main_content` (default `true`) il chrome del sito (navigazione, header, footer, sidebar, banner dei cookie, nodi nascosti) viene rimosso dietro una guardia di fedeltà, e gli URL delle immagini vengono resi come il loro testo alt. I blocchi di codice mantengono il loro linguaggio sul fence (` ```python `) in entrambe le modalità. Imposta `only_main_content: false` per la pagina completa. Vedi [Qualità del Markdown](#qualita-del-markdown). |
| `html_cleaned` | URL firmato | L'HTML renderizzato con script, stili e boilerplate rimossi. |
| `html_raw` | URL firmato | L'HTML renderizzato completo, esattamente come prodotto dal browser. |
| `screenshot` | URL firmato | Un PNG del viewport, nella dimensione richiesta (o predefinita). |
| `screenshot_full_page` | URL firmato | Un PNG a pagina intera che cattura l'intera altezza di scroll. |
| `pdf` | URL firmato | Un PDF della pagina. Accetta l'intera superficie di `pdf_options` (vedi sotto). |
| `links` | URL firmato | Un array JSON di ogni link trovato, con URL assoluti e testo dell'ancora. |
| `images` | URL firmato | Un array JSON di ogni immagine, con `src` assoluto e testo `alt`. |
| `structured` | JSON inline | Dati strutturati estratti dalla pagina (il campo di risposta `structured`). |

### Qualità del Markdown

Prima che la pagina venga convertita, il DOM renderizzato viene
normalizzato affinché il Markdown rifletta ciò che vede un lettore e non
come la pagina è stata costruita. Questo avviene a ogni render, quindi
il risultato non dipende da quale strategia di estrazione vince per una
determinata pagina.

Sempre applicato, in entrambe le modalità di `only_main_content`:

- **I fence di codice mantengono il loro linguaggio.** Il linguaggio
  viene letto dalla convenzione usata dal sito
  (`class="language-python"`, `data-lang`, un attributo `language`
  nudo, o un wrapper dell'evidenziatore) e normalizzato, così arriva
  ` ```python ` invece di un fence spoglio.
- **I link delle card restano leggibili.** Un link che avvolge un
  titolo e una descrizione diventa un titolo collegato seguito dalla
  sua descrizione, invece di un unico link appiccicato come
  `[DatabaseSupabase provides a full Postgres database...]`. L'URL di
  destinazione viene preservato.
- **I titoli restano su una sola riga.** Un titolo il cui testo si
  trova dentro un elemento annidato non emette più un `##` spoglio con
  il testo abbandonato sotto.
- **Gli elementi adiacenti non si concatenano più.** I layout che
  spaziano i loro elementi con il CSS anziché con spazi producevano
  `YesNo` ed `EvaluationDeploymentProduction`; ora si leggono come
  parole separate.
- **I caratteri invisibili vengono rimossi**: spazi a larghezza zero
  usati come etichette di ancoraggio, trattini morbidi e glifi della
  Private Use Area dei font di icone, che arrivano come token non
  stampabili.
- **Gli elementi vuoti vengono scartati**: elementi `<i>` con la sola
  icona che venivano resi come `__` vaganti, e link con etichetta
  vuota.

Inoltre, con `only_main_content: true`:

- **I controlli di interfaccia vengono rimossi**: pulsanti, barre di
  tab, suggerimenti di scorciatoie da tastiera, azioni «Copy page» /
  «On this page» e widget di valutazione «Questa pagina ti è stata
  utile? Sì/No». Un controllo che porta contenuto reale (una domanda
  di FAQ, il corpo di una card cliccabile) viene mantenuto.
- **Il testo destinato ai soli screen reader viene rimosso**: skip link
  e le etichette «Section titled ...» che molti temi di documentazione
  attaccano a ogni titolo.
- **Il non-contenuto dichiarato dal sito viene rispettato**: blocchi
  marcati `data-nosnippet`, `data-pagefind-ignore` o `data-noindex`, a
  meno che non contengano titoli o codice.
- **I blocchi duplicati vengono uniti**: i design responsive che
  spediscono una copia desktop e una mobile della stessa barra, e i
  caroselli che pre-renderizzano ogni fotogramma, compaiono una volta
  sola.
- **Breadcrumb ed etichette di occhiello sopra il titolo della pagina
  vengono scartati.**

Il contenuto differito viene *deliberatamente* mantenuto: un pannello
di tab inattivo all'interno della regione di contenuto contiene un vero
esempio di codice (l'esempio Python in un tab, quello JavaScript in un
altro), così entrambi arrivano nel Markdown e non solo il tab che al
momento del render risultava selezionato.

### Rendering e attesa

| Parametro | Tipo | Predefinito | Descrizione |
|-----------|------|---------|-------------|
| `viewport` | `object` | `1920 x 1080` | `{"width": <int>, "height": <int>}`. Larghezza 320–3840, altezza 240–2160. |
| `mobile` | `boolean` | `false` | Esegue il render con un viewport mobile (390 x 844) a meno che `viewport` non sia impostato esplicitamente. |
| `wait_for` | `string` | `null` | Attende dopo la navigazione un selettore CSS (`".price"` o `"css:.price"`) o un'espressione JS (`"js:window.dataReady === true"`). |
| `wait_timeout_ms` | `integer` | `30000` | Per quanto tempo `wait_for` può attendere, in millisecondi. 0–60,000. Un timeout degrada a un warning; la pagina viene catturata così com'è. |
| `js_code` | `string` | `null` | JavaScript da eseguire sulla pagina dopo la navigazione. Massimo 20,000 caratteri. Un errore diventa un warning, non un fallimento. |
| `block_resources` | `string[]` | `[]` | Tipi di risorsa da interrompere prima che si carichino. Uno tra `image`, `media`, `font`, `stylesheet`, `script`, `xhr`, `fetch`, `websocket`, `manifest`, `other`. Utile per render più veloci, solo testo. |
| `respect_robots` | `boolean` | `false` | Quando è `true`, un URL non consentito dal `robots.txt` del sito viene rifiutato con `403`. |
| `pdf_options` | `object` | `null` | Formato pagina, margini, header, footer, scala e orientamento per l'output `pdf`. Stesso oggetto di [url-to-pdf](/it/docs/endpoints/convert/web-pages/url-to-pdf.md). Senza `pdf_options`, perceive produce una singola pagina continua, byte-identica a V1 url-to-pdf. |

### Richieste autenticate e personalizzate

| Parametro | Tipo | Predefinito | Descrizione |
|-----------|------|---------|-------------|
| `auth` | `object` | `null` | HTTP Basic Auth per la pagina di destinazione: `{"username": "...", "password": "..."}`. |
| `cookies` | `array` | `null` | Cookie da iniettare prima della navigazione. Massimo 50. Ognuno richiede `name`, `value`, e uno tra `domain` o `url`. |
| `headers` | `object` | `null` | Header di richiesta personalizzati. Massimo 20. Nomi bloccati: `host`, `content-length`, `transfer-encoding`, `connection`, `upgrade`, `te`, `trailer`. |

<div class="alert alert-warning">
<strong>Riservati, non ancora attivi.</strong> <code>proxy_url</code> (Production+),
<code>geolocation</code> e <code>action_chain</code> sono accettati dallo
schema della richiesta ma oggi restituiscono <code>422</code>. Arriveranno in
una release futura; inviarli ora ti dice esattamente quale funzione non è
pronta invece di ignorarla silenziosamente.
</div>

---

## Estrazione strutturata

Quando `structured` è presente in `outputs`, la lista `extract` controlla
quali campi perceive estrae. Se non richiedi nulla, il default è
`metadata` e `structured_data`.

| Valore di `extract` | Campo in `structured` | Stato |
|-----------------|-----------------------|--------|
| `metadata` | `metadata` | Attivo |
| `structured_data` | `structured_data` (JSON-LD) | Attivo |
| `headings` | `headings` | Attivo |
| `tables` | `tables` | Attivo |
| `main_content` | `main_content` (testo, limitato a 50,000 caratteri) | Attivo |
| `all` | si espande in tutti i campi attivi sopra elencati | Attivo |
| `prices` | -- | Non ancora attivo: restituisce un warning, omesso |
| `contacts` | -- | Non ancora attivo: restituisce un warning, omesso |
| `technologies` | -- | Non ancora attivo: restituisce un warning, omesso |

Per essere diretti: `prices`, `contacts` e `technologies` sono nomi
riservati. Richiederne uno oggi non genera un errore. Il nome finisce
nell'array `warnings` e viene rimosso da `structured`.

### Estrazione guidata da schema

Invia uno `schema` per estrarre campi specifici in `structured.extracted`:

```json
{
    "url": "https://example.com/product/widget",
    "outputs": ["markdown", "structured"],
    "schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "price": {"type": "number"},
            "in_stock": {"type": "boolean"}
        }
    }
}
```

Il livello di estrazione assistita da LLM completa lo schema e
si attiva solo quando valgono **tutte** queste condizioni: hai inviato uno
`schema`, il tuo piano include il livello LLM (Indie e superiori), la
pagina non è stata valutata come bloccata, e il passaggio euristico ha
lasciato vuoti i campi dello schema. Quando viene eseguito,
`extraction_tier` vale `"llm"`, e `tokens` e `cost_cents` riportano il
costo di quell'estrazione; altrimenti `extraction_tier` vale `"heuristic"`
ed entrambi sono zero.

> **Nota.** L'estrazione tramite schema ha un tetto rigido per proteggere
> la tua spesa: una singola estrazione è limitata per richiesta, e la spesa
> del progetto attinge dal tuo saldo mensile di crediti AI ($5 / $15 / $40
> al mese su Indie / Studio / Production; i crediti non usati si
> accumulano). L'estrazione LLM consuma crediti, non ops. Se viene
> raggiunto un tetto o il saldo è esaurito, perceive restituisce il
> risultato euristico con una nota in `warnings` invece di spendere oltre
> il limite. Su un piano senza livello
> LLM, ottieni solo dati `structured` euristici.

---

## Risposta {: #response }

Sia `POST /v2/perceive` che `GET /v2/perceive/{operation_id}` restituiscono
lo stesso oggetto.

| Campo | Tipo | Descrizione |
|-------|------|-------------|
| `operation_id` | `string` | ID opaco (`per_...`). Usalo con l'endpoint GET e citalo quando contatti il supporto. |
| `status` | `string` | `queued`, `processing`, `completed`, o `failed`. |
| `url` | `string` | L'URL che hai inviato. |
| `url_final` | `string` | L'URL dopo i redirect. |
| `content_hash` | `string` | SHA-256 della pagina renderizzata. Determina la cache di 1 ora. |
| `render_quality` | `number` | 0.0–1.0. Punteggi sotto 0.40 indicano un render fallito: una pagina anti-bot, un login wall, una pagina di errore HTTP, un soft 404 o un guscio vuoto. Consulta `deductions` per il motivo e `status_code` per lo stato del server di origine. |
| `status_code` | `integer` | Stato HTTP della risposta finale del documento principale (es. `200`, `404`). `null` quando è sconosciuto. |
| `deductions` | `object` | Detrazioni di render-quality con nome che sono scattate, es. `{"http_error": 0.7}`, `{"soft_404": 0.65}`, `{"login_wall": 0.65}`. Vuoto per un render pulito. |
| `options_echo` | `object` | Eco delle opzioni della richiesta che il server ha applicato. I segreti sono ridotti a booleani (`auth_provided`, `cookies_provided`, `headers_provided`, `js_code_provided`, `schema_provided`, `pdf_options_provided`). Le opzioni semplici (`outputs`, `only_main_content`, `truncate_data_arrays`, `allow_degraded`, `extract`, `cache_mode`, `mobile`, `respect_robots`, `direct_download`, `wait_for`, `wait_timeout_ms`, `viewport`, `block_resources`) vengono restituite così come applicate. `truncate_data_arrays` viene restituito come booleano **risolto**, così anche lasciandolo non impostato sai in che modo è stato deciso. |
| `cache_hit` | `boolean` | `true` quando il risultato proviene dalla cache invece che da un render fresco. |
| `outputs` | `object` | Mappa da nome dell'output a `{url, object_key, size_bytes, content_type, expires_in}`. Gli URL firmati scadono in 900 secondi. |
| `structured` | `object` | Dati strutturati inline, presenti quando è stato richiesto `structured`. |
| `extraction_tier` | `string` | `heuristic`, `css`, o `llm`. |
| `tokens` | `object` | Token LLM `{input, output}` utilizzati. Zero a meno che il livello LLM non sia stato eseguito. |
| `cost_cents` | `number` | Costo LLM in centesimi per questa operazione. Zero a meno che il livello LLM non sia stato eseguito. |
| `duration_ms` | `integer` | Tempo di render end-to-end. |
| `error` | `string` | Impostato solo quando `status` è `failed`. |
| `warnings` | `string[]` | Note non fatali: un timeout di `wait_for`, un extract saltato, un flag di pagina bloccata, un fallback di `only_main_content` alla pagina completa, una nota che lunghi array di dati numerici sono stati troncati. |

---

## Recuperare un'operazione {: #retrieve-an-operation }

Gli URL firmati scadono dopo 15 minuti. Per scaricare un output più tardi,
recupera di nuovo l'operazione e perceive firma di nuovo ogni URL a
partire dagli object key salvati. Non avviene alcun nuovo render, quindi
questo non consuma ops.

```bash
curl https://api.enconvert.com/v2/perceive/per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key"
```

Un operation ID sconosciuto, o uno che appartiene a un progetto diverso,
restituisce `404`. L'esistenza non viene mai rivelata tra progetti.

---

## Download diretto

Di default ogni output file torna come URL pre-firmato da recuperare con
una seconda richiesta. Imposta `direct_download: true` sul POST per
saltare la busta: il body della risposta HTTP **è** i byte
dell'artefatto, senza JSON, senza URL firmato e senza una seconda
richiesta. La richiesta deve produrre esattamente un output che generi un
artefatto (`outputs: ["markdown"]`, `outputs: ["pdf"]`, …), altrimenti
viene rifiutata con `400`. I metadata che sarebbero finiti nel JSON
viaggiano invece negli header della risposta: `Content-Disposition`,
`X-Operation-Id`, `X-Object-Key`, `X-Cache-Hit`, `X-Render-Quality`,
`X-Source-Status-Code`, `X-Content-Hash` e `X-Warnings-Count`.

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/blog/post",
    "outputs": ["markdown"],
    "direct_download": true
  }' \
  -o post.md
```

Gli endpoint GET trasmettono gli artefatti salvati allo stesso modo:

- `GET /v2/perceive/{operation_id}?direct_download=true&output=markdown`
  trasmette un artefatto di un'operazione passata. `output` è
  obbligatorio quando l'operazione ha prodotto più di un artefatto. Un
  artefatto oltre la finestra di retention del tuo piano risponde `410`.
- `GET /v2/perceive/batch/{job_id}?direct_download=true` trasmette lo
  ZIP del batch per i batch con `output_mode: "zip"` il cui archivio è
  pronto, e negli altri casi risponde `400`.

`direct_download` vale solo per URL singoli: `POST /v2/perceive/batch`
lo rifiuta con `422`. Imposta `output_mode` su `"zip"` e scarica
l'archivio. Vedi [Perceive in batch](#batch-perception).

---

## Perceive in batch {: #batch-perception }

`POST /v2/perceive/batch` esegue il perceive di un elenco di URL che
condividono un unico blocco `options`. Ogni URL viene renderizzato tramite
la stessa pipeline di una singola chiamata e produce la propria riga di
operazione.

```bash
curl -X POST https://api.enconvert.com/v2/perceive/batch \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com/a",
      "https://example.com/b",
      "https://example.com/c"
    ],
    "options": {"outputs": ["markdown"]},
    "output_mode": "manifest"
  }'
```

I batch di 10 URL o meno vengono eseguiti inline e rispondono `200` con
ogni risultato popolato. I batch più grandi rispondono `202` con un
`job_id`; gli URL vengono processati uno alla volta e puoi effettuare il
polling dei risultati:

```bash
curl https://api.enconvert.com/v2/perceive/batch/{job_id} \
  -H "X-API-Key: sk_your_private_key"
```

La risposta del batch riporta il progresso aggregato e include un
risultato perceive completo per URL una volta renderizzato:

```json
{
    "job_id": "bat_8c1a...",
    "status": "partial",
    "output_mode": "manifest",
    "total": 3,
    "completed": 2,
    "failed": 1,
    "pending": 0,
    "items": [
        {"operation_id": "per_...", "status": "completed", "url": "https://example.com/a"},
        {"operation_id": "per_...", "status": "completed", "url": "https://example.com/b"},
        {"operation_id": "per_...", "status": "failed", "url": "https://example.com/c"}
    ]
}
```

`status` è `queued`, `processing`, `completed`, `failed`, `partial`
(alcuni URL riusciti, altri falliti), o `canceled`. Imposta `output_mode`
su `zip` per raggruppare ogni artefatto in un unico ZIP, restituito nel
campo `zip` una volta terminato il batch.

### Durevole e ripristinabile

I batch sono a prova di riavvio. Se il servizio si riavvia mentre un batch
è in corso, il batch **riprende automaticamente** e renderizza di nuovo
solo gli URL che non erano stati completati, così gli URL già completati
mantengono i loro artefatti. Non devi mai reinviare un batch a causa di un
riavvio.

### Annullare un batch

`DELETE /v2/perceive/batch/{job_id}` annulla un batch in esecuzione. Il
worker si ferma tra un URL e l'altro, quindi gli URL già renderizzati
mantengono i loro risultati e i rimanenti restano non avviati. La chiamata
è idempotente, quindi annullare un batch già terminato restituisce
semplicemente il suo stato attuale, e lo `status` del batch diventa
`canceled`.

```bash
curl -X DELETE https://api.enconvert.com/v2/perceive/batch/{job_id} \
  -H "X-API-Key: sk_your_private_key"
```

---

## Caching

`cache_mode` controlla come perceive tratta la sua cache dei risultati di
1 ora, la cui chiave è composta dal tuo progetto, dall'URL e dalle opzioni
della richiesta che influenzano il render.

| `cache_mode` | Comportamento |
|--------------|-----------|
| `enabled` (predefinito) | Restituisce un risultato in cache quando una richiesta identica è stata renderizzata nell'ultima ora. `cache_hit` è `true`, `cost_cents` è `0`. |
| `bypass` | Salta la cache ed esegue un render fresco. |
| `refresh` | Esegue un render fresco e sostituisce la voce in cache. |

Da segnalare: un cache hit conta comunque come un'operazione perceive
rispetto alla tua quota mensile di ops. La quota misura le operazioni anziché i
render del browser, quindi la cache ti fa risparmiare tempo di render, non
quota.

---

## Esempi di codice

### curl: solo Markdown

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/blog/post",
    "outputs": ["markdown"]
  }'
```

### curl: Markdown più dati strutturati

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown", "structured"],
    "extract": ["metadata", "structured_data", "tables"]
  }'
```

### curl: output completi più PDF

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/report",
    "outputs": ["markdown", "html_cleaned", "screenshot_full_page", "pdf"],
    "pdf_options": {"format": "A4", "print_background": true}
  }'
```

### Python

```python
import requests

response = requests.post(
    "https://api.enconvert.com/v2/perceive",
    headers={"X-API-Key": "sk_your_private_key"},
    json={
        "url": "https://example.com/pricing",
        "outputs": ["markdown", "structured"],
        "extract": ["metadata", "tables"],
    },
)
response.raise_for_status()
data = response.json()

# Download the Markdown artifact from its signed URL
markdown_url = data["outputs"]["markdown"]["url"]
markdown_text = requests.get(markdown_url).text

print(data["structured"])
print(markdown_text)
```

### Node.js

```javascript
const res = await fetch("https://api.enconvert.com/v2/perceive", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_your_private_key"
    },
    body: JSON.stringify({
        url: "https://example.com/pricing",
        outputs: ["markdown", "structured"],
        extract: ["metadata", "tables"]
    })
});

const data = await res.json();

// Download the Markdown artifact from its signed URL
const markdownText = await fetch(data.outputs.markdown.url).then(r => r.text());

console.log(data.structured);
console.log(markdownText);
```

Se chiami EnConvert da Claude, Cursor o un altro client MCP, la stessa
funzionalità è esposta come tool `perceive_url`. Vedi
[la pagina del server MCP](/it/mcp.md).

---

## Risposte di errore

| Stato | Condizione |
|--------|-----------|
| `400 Bad Request` | L'URL non è `http(s)`, contiene credenziali incorporate, o si risolve in un indirizzo privato, loopback o link-local (protezione SSRF). |
| `400 Bad Request` | `auth` non valido (manca `username`/`password`), `cookies` non valido (non è un array, oltre 50 voci, campi mancanti), o `headers` non valido (non è un oggetto, oltre 20 voci, nome bloccato). |
| `401 Unauthorized` | Chiave API / token JWT mancante o non valido. |
| `402 Payment Required` | Perceive non è incluso nel tuo piano attuale, oppure la tua quota mensile di ops è esaurita. |
| `403 Forbidden` | `/v2/perceive` non è tra gli endpoint consentiti della chiave API. |
| `403 Forbidden` | Il batch non è disponibile sul tuo piano, oppure la dimensione del batch supera il limite del tuo piano. |
| `403 Forbidden` | `respect_robots=true` e il `robots.txt` del sito non consente l'URL. |
| `404 Not Found` | `operation_id` o `job_id` sconosciuto, oppure di proprietà di un altro progetto. |
| `422 Unprocessable Entity` | Validazione della richiesta fallita (enum non valido in `outputs`/`extract`, `wait_timeout_ms` fuori range, viewport fuori dai limiti, una chiave della richiesta sconosciuta). |
| `422 Unprocessable Entity` | È stato inviato `proxy_url`, `geolocation`, o `action_chain`. Tutti e tre sono riservati per una release futura. |
| `500 Internal Server Error` | Il render è fallito. Il messaggio include l'`operation_id` da citare al supporto. |
| `502 Bad Gateway` | Tutti i motori sono stati bloccati e l'origine ha restituito una challenge anti-bot senza contenuto di pagina dietro di essa. Riprova più tardi, oppure invia `allow_degraded: true` per ricevere la pagina della challenge così com'è. |

Le chiavi della richiesta sconosciute vengono rifiutate con un `422` che
indica il campo, su `/v2/perceive`, `/v2/perceive/batch`, `/v2/discover`
e `/v2/lookup` allo stesso modo. Non vengono mai ignorate silenziosamente.
Ogni body `422` include un array `errors` di primo livello con messaggi
leggibili, accanto alla lista `detail` grezza.

Il riferimento completo dei codici di stato è nella
[guida ai codici di errore](/it/docs/reference/errors.md).

---

## Limiti

| Limite | Valore |
|-------|-------|
| Lunghezza URL | 2,048 caratteri |
| `wait_timeout_ms` | 0–60,000 ms |
| Lunghezza `js_code` | 20,000 caratteri |
| Larghezza viewport | 320–3,840 px |
| Altezza viewport | 240–2,160 px |
| Cookie per richiesta | 50 |
| Header personalizzati per richiesta | 20 |
| Extract `main_content` | 50,000 caratteri |
| URL batch per richiesta | 1,000 (tetto dello schema) |
| Soglia batch inline | 10 URL (i batch più grandi vengono eseguiti in modo asincrono) |
| TTL della cache dei risultati | 1 ora |
| Scadenza URL firmato | 15 minuti |
| Ops mensili (condivise tra tutti gli endpoint) | 500 / 3.000 / 15.000 / 50.000 per livello; vedi [i prezzi](/it/pricing.md) |

---

## Domande frequenti

### Come converto una pagina web in Markdown con una API REST?

Invia `POST /v2/perceive` con `{"url": "...", "outputs": ["markdown"]}`. La pagina viene renderizzata in Chrome headless e la risposta include un URL di download pre-firmato per il file Markdown. Di default `only_main_content` rimuove il chrome del sito, così ricevi l'articolo, non la navigazione; imposta `"only_main_content": false` per la pagina completa, oppure aggiungi `"direct_download": true` per ricevere i byte del Markdown direttamente nel body della risposta.

### Posso ottenere uno screenshot e il Markdown dallo stesso render?

Sì. `outputs` accetta qualsiasi combinazione, quindi `["markdown", "screenshot"]` (o `screenshot_full_page` per l'intera altezza di scroll) produce entrambi da un singolo render del browser. Non paghi mai la stessa pagina due volte in un'unica chiamata.

### `/v2/perceive` renderizza pagine JavaScript?

Sì. Ogni richiesta esegue un render reale in Chrome headless: i banner dei cookie vengono chiusi, la pagina viene scrollata per attivare i contenuti lazy, e puoi controllare la pagina prima della cattura con `wait_for` (un selettore CSS o un'espressione JS), `js_code`, e `block_resources`.

### Perché il mio URL di download firmato ha smesso di funzionare?

Gli URL firmati scadono dopo 15 minuti (`expires_in: 900`). Recupera di nuovo l'operazione con `GET /v2/perceive/{operation_id}` per ottenere URL firmati di fresco. Non avviene alcun nuovo render e non vengono consumate ops.

### Un risultato in cache conta comunque nella mia quota?

Sì. Un cache hit addebita una op, perché la quota mensile misura le operazioni anziché i render del browser. Imposta `cache_mode` su `bypass` per saltare la cache di 1 ora, oppure su `refresh` per eseguire un render fresco e sostituire la voce in cache.
