---
seo_title: SDK Node.js per conversione file, npm | EnConvert
meta_desc: Installa @enconvert/node-sdk da npm per Node.js 18+. Metodi di conversione file tipizzati e un namespace client.v2 per perceive, discover, distill, ingest e watch.
keywords: sdk conversione file nodejs, client npm api conversione file, convertire file con nodejs, sdk url in pdf nodejs, heic in webp nodejs, docx in pdf node js, comprimere immagini nodejs, qualsiasi file in markdown nodejs, qualsiasi file in pdf nodejs, client api typescript conversione, enconvert node sdk, sdk web scraping nodejs, url in markdown nodejs, estrazione dati strutturati nodejs, pipeline rag nodejs, monitoraggio modifiche sito nodejs
---

# SDK Node.js per la conversione file

`@enconvert/node-sdk` è il client JavaScript e TypeScript ufficiale per l'API EnConvert. Tredici metodi di conversione tipizzati corrispondono 1:1 agli endpoint REST come `POST /v1/convert/url-to-pdf`, e un secondo namespace, `client.v2`, aggiunge la web intelligence: percepire un URL in artefatti pronti per gli agenti, scoprire gli URL di un sito, eseguire una ricerca web, distillare dati strutturati, ingerire un sito in JSONL pronto per RAG e sorvegliare le pagine alla ricerca di cambiamenti. Richiede Node.js 18+ con zero dipendenze runtime, è costruito su `fetch`, `FormData` e `node:stream` nativi e recupera in modo trasparente i timeout del reverse-proxy facendo polling sullo stato del job. Viene distribuito con build ESM e CJS duali e dichiarazioni TypeScript complete.

<div class="alert alert-info">
<strong>npm:</strong> <code>@enconvert/node-sdk</code> · <strong>Sorgente:</strong> <a href="https://github.com/enconvert/node-sdk">enconvert/node-sdk</a> · <strong>Node:</strong> 18+
</div>

---

## Installazione

```bash
npm install @enconvert/node-sdk
```

```bash
pnpm add @enconvert/node-sdk
```

```bash
yarn add @enconvert/node-sdk
```

---

## Guida rapida

```ts
import { Enconvert } from "@enconvert/node-sdk";

const client = new Enconvert({ apiKey: process.env.ENCONVERT_API_KEY! });

// V1: converte un URL in PDF e lo scrive su disco in streaming.
const result = await client.convertUrlToPdf("https://example.com", {
    saveTo: "page.pdf",
});
console.log(result.presignedUrl);

// V2: legge una pagina come dovrebbe farlo il tuo agente, con un punteggio di qualità allegato.
const op = await client.v2.perceive("https://example.com", {
    outputs: ["markdown", "structured"],
});
console.log(op.outputs.markdown.url, op.renderQuality); // ad es. 0.93
```

L'SDK funziona su ogni runtime Node moderno (Node 18+, Bun, Deno tramite lo specificatore npm). È **solo lato server**, quindi non includere la tua chiave API privata nel bundle di un'applicazione browser.

---

## Cosa espone il client

Un solo client, due superfici. Entrambe si raggiungono dalla stessa istanza `Enconvert` e condividono un'unica chiave API.

| Superficie | Come si raggiunge | Cosa copre |
|---------|-----------|----------------|
| Conversione file | `client.convertUrlToPdf(...)`, `client.convertImage(...)` e così via | Tredici metodi tipizzati per il rendering di URL, la conversione di immagini, la compressione di immagini, la conversione di documenti, più il polling dei job e dei batch su interi siti. Vedi [Conversione file](#conversione-file). |
| Web intelligence (V2) | `client.v2.perceive(...)`, `client.v2.distill(...)` e così via | Ventitré metodi su sei capacità: perceive, discover, lookup, distill, ingest, watch. Vedi [Web intelligence (V2)](#web-intelligence-v2). |

Gli endpoint V2 richiedono una chiave API privata (`sk_...`); le chiavi pubbliche vengono rifiutate. Consulta [Autenticazione](/it/docs/authentication.md) per capire in cosa differiscono i due tipi di chiave, e la [V1 e V2](/it/docs/concepts/v1-and-v2.md) per la superficie REST che sta dietro a `client.v2`.

---

## Conversione file

La superficie di conversione espone tredici metodi che corrispondono 1:1 all'API REST:

| Metodo | Endpoint | Restituisce |
|--------|----------|---------|
| `convertUrlToPdf(url, options?)` | `POST /v1/convert/url-to-pdf` | `ConversionResult` |
| `convertUrlToScreenshot(url, options?)` | `POST /v1/convert/url-to-screenshot` | `ConversionResult` |
| `convertUrlToMarkdown(url, options?)` | `POST /v1/convert/url-to-markdown` | `ConversionResult` |
| `convertImage(file, options)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `convertDocument(file, options?)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `compressImage(file, options?)` | `POST /v1/convert/compress-image` | `ConversionResult` |
| `convertToMarkdown(file, options?)` | `POST /v1/convert/anything-to-markdown` | `ConversionResult` |
| `convertToPdf(file, options?)` | `POST /v1/convert/anything-to-pdf` | `ConversionResult` |
| `getJobStatus(jobId)` | `GET /v1/convert/status/{jobId}` | `JobStatus` |
| `convertWebsiteToPdf(url, options?)` | `POST /v1/convert/website-to-pdf` | `BatchSubmission` |
| `convertWebsiteToScreenshot(url, options?)` | `POST /v1/convert/website-to-screenshot` | `BatchSubmission` |
| `getBatchStatus(batchId)` | `GET /v1/convert/batch/{batchId}` | `BatchStatus` |
| `waitForBatch(batchId, options?)` | `GET /v1/convert/batch/{batchId}` (con polling) | `BatchStatus` |

Ogni metodo restituisce una promise tipizzata. Tutti i campi delle opzioni sono facoltativi se non indicato diversamente. Gli ultimi quattro sono helper batch per interi siti: inviano e interrogano job asincroni, quindi restituiscono un `BatchSubmission` o un `BatchStatus` invece di un `ConversionResult`.

---

### `convertUrlToPdf`

Esegue il rendering in PDF di qualsiasi URL pubblico.

```ts
const result = await client.convertUrlToPdf("https://example.com", {
    pdfOptions: { pageSize: "A4", orientation: "landscape" },
    singlePage: false,
    viewportWidth: 1440,
    saveTo: "report.pdf",
});
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `saveTo` | `string` | -- | Percorso locale su cui scrivere il PDF in streaming. Le directory superiori vengono create automaticamente. |
| `singlePage` | `boolean` | `true` | `true` produce una sola pagina continua. `false` impagina usando `pdfOptions.pageSize`. |
| `pdfOptions` | `PdfOptions` | -- | Dimensione pagina, orientamento, margini, scala, scala di grigi, intestazione e piè di pagina. Vedi [Opzioni PDF](#opzioni-pdf). |
| `viewportWidth` | `number` | `1920` | Larghezza del viewport del browser in pixel. |
| `viewportHeight` | `number` | `1080` | Altezza del viewport del browser in pixel. |
| `loadMedia` | `boolean` | `true` | Attende immagini e video prima della cattura. |
| `enableScroll` | `boolean` | `true` | Scorre dall'alto in basso per far scattare i lazy loader. |
| `outputFilename` | `string` | automatico | Sovrascrive il nome file generato. `.pdf` viene aggiunto se manca. |

---

### `convertUrlToScreenshot`

Cattura un PNG a pagina intera di qualsiasi URL.

```ts
const result = await client.convertUrlToScreenshot("https://example.com", {
    viewportWidth: 1440,
    saveTo: "screenshot.png",
});
```

Accetta le stesse opzioni di viewport, media, scroll e nome file di `convertUrlToPdf` (esclusi `singlePage` e `pdfOptions`).

---

### `convertUrlToMarkdown`

Estrae Markdown GitHub-Flavored pulito da qualsiasi URL. Il convertitore rimuove navigazione, piè di pagina, pubblicità e script, mantiene il corpo principale dell'articolo e antepone un frontmatter YAML (titolo, descrizione, url, link, immagini).

```ts
const result = await client.convertUrlToMarkdown("https://example.com/article", {
    saveTo: "article.md",
});
```

Utile per costruire pipeline RAG, importare contenuti di terze parti in un CMS o generare dati di addestramento. Se vuoi un punteggio di qualità del render insieme al Markdown, usa invece [`client.v2.perceive`](#perceive).

---

### `convertImage`

Converte tra `jpeg`, `png`, `svg`, `heic` e `webp`.

```ts
// Da un percorso
await client.convertImage("photo.heic", {
    outputFormat: "webp",
    saveTo: "photo.webp",
});

// Da byte
import { readFile } from "node:fs/promises";
const buf = await readFile("photo.heic");

await client.convertImage(
    { data: buf, filename: "photo.heic" },
    { outputFormat: "webp", saveTo: "photo.webp" },
);

// Rasterizza un SVG a una larghezza fissa
await client.convertImage("logo.svg", { outputFormat: "png", width: 512, saveTo: "logo.png" });
```

Il formato di input viene rilevato dall'estensione del percorso o del nome file. Il formato di output è obbligatorio.

| Opzione | Tipo | Obbligatoria | Descrizione |
|--------|------|----------|-------------|
| `outputFormat` | `string` | Sì | Formato di destinazione: `jpeg`, `png`, `svg`, `heic` o `webp` (e `jpeg` per un input `.pdf`). Gli alias `jpg`, `yml`, `htm` e `md` vengono normalizzati. Le coppie non supportate sollevano un errore prima che la richiesta parta. |
| `saveTo` | `string` | -- | Percorso locale su cui scrivere il risultato in streaming. |
| `outputFilename` | `string` | -- | Sovrascrive il nome file generato. |
| `width` | `number` | -- | Solo per input SVG (`svg-to-png`, `svg-to-jpeg`, `svg-to-webp`), da 1 a 10000. Da sola scala in modo proporzionale, ricavando l'altezza dalle proporzioni dell'SVG. |
| `height` | `number` | -- | Solo per input SVG (`svg-to-png`, `svg-to-jpeg`, `svg-to-webp`), da 1 a 10000. Da sola scala in modo proporzionale, ricavando la larghezza dalle proporzioni dell'SVG. |

Imposta sia `width` sia `height` per fissare un canvas esatto, cosa che può cambiare le proporzioni. Ometti entrambe e l'output conserva la larghezza, l'altezza o il `viewBox` intrinseci dell'SVG. Il totale dei pixel in uscita è limitato a 25.000.000. Nessuna delle due opzioni è accettata da `svg-to-heic`, e l'SDK solleva un errore prima di inviare la richiesta se le passi a qualsiasi altra conversione.

---

### `convertDocument`

Converte documenti e formati dati. Il valore predefinito di `outputFormat` è `"pdf"`.

```ts
// docx in pdf
await client.convertDocument("report.docx", { saveTo: "report.pdf" });

// json in yaml
await client.convertDocument("data.json", {
    outputFormat: "yaml",
    saveTo: "data.yaml",
});

// markdown in pdf con impostazioni di pagina personalizzate
await client.convertDocument("README.md", {
    outputFormat: "pdf",
    pdfOptions: { pageSize: "A4", margins: { top: 20, bottom: 20, left: 25, right: 25 } },
    saveTo: "readme.pdf",
});
```

**Input supportati:** `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`, `html`, `htm`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers`, `markdown` (`.md`, `.markdown`), `csv`, `json`, `xml`, `yaml` (`.yaml`, `.yml`), `toml`.

EPUB non ha una coppia di conversione documenti dedicata. Passa i file `.epub` attraverso [`convertToPdf`](#converttopdf) o [`convertToMarkdown`](#converttomarkdown).

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `outputFormat` | `string` | `"pdf"` | Formato di destinazione. |
| `saveTo` | `string` | -- | Percorso locale su cui scrivere il risultato in streaming. |
| `outputFilename` | `string` | -- | Sovrascrive il nome file generato. |
| `pdfOptions` | `PdfOptions` | -- | Impostazioni di pagina. Considerate solo quando l'output è PDF. |

---

### `compressImage`

Riduce un PNG, un JPEG o un WebP senza cambiarne il formato.

```ts
// Solo passaggio lossless
const result = await client.compressImage("photo.jpg", { saveTo: "photo-small.jpg" });

// Punta a un budget di 200 KB
const capped = await client.compressImage("photo.jpg", {
    targetSizeKb: 200,
    saveTo: "photo-capped.jpg",
});

console.log(capped.fileSize);
```

**Input supportati:** `.png`, `.jpg`, `.jpeg`, `.webp`.

L'output conserva il formato e l'estensione dell'input, quindi non c'è un formato di destinazione da scegliere. Il primo stadio è lossless: i metadati vengono rimossi, il profilo ICC e l'orientamento EXIF vengono preservati, e il risultato non è mai più grande dell'input. Impostare `targetSizeKb` aggiunge un secondo stadio che riduce le dimensioni mantenendo bloccate le proporzioni finché il budget non è rispettato. Quel target è "best effort": un budget irraggiungibile restituisce il file più piccolo ottenuto invece di un errore, quindi controlla `result.fileSize`. APNG animate e WebP animate vengono rifiutate con `400`, e il canvas decodificato è limitato a 40.000.000 di pixel.

| Opzione | Tipo | Obbligatoria | Descrizione |
|--------|------|----------|-------------|
| `targetSizeKb` | `number` | -- | Budget di dimensione in KB, intero, minimo `1`. Omettilo per eseguire solo il passaggio lossless. |
| `saveTo` | `string` | -- | Percorso locale su cui scrivere il risultato in streaming. |
| `outputFilename` | `string` | -- | Sovrascrive il nome file generato. L'estensione dell'input viene mantenuta. |

---

### `convertToMarkdown`

Converte in Markdown qualsiasi documento, foglio di calcolo, presentazione, ebook, file web o file di testo semplice supportato.

```ts
await client.convertToMarkdown("handbook.docx", {
    saveTo: "handbook.md",
});
```

**Input supportati (22):** `.csv`, `.doc`, `.docx`, `.epub`, `.htm`, `.html`, `.markdown`, `.md`, `.mdown`, `.mkd`, `.odp`, `.ods`, `.odt`, `.pdf`, `.ppt`, `.pptx`, `.rtf`, `.text`, `.txt`, `.xhtml`, `.xls`, `.xlsx`.

L'output è un unico file `.md` consapevole delle intestazioni e pensato per il chunking RAG: la gerarchia di intestazioni del documento sopravvive alla conversione, quindi un chunker semantico può dividere sulle intestazioni invece che su conteggi arbitrari di caratteri. Su questo endpoint non ci sono opzioni PDF. Qualsiasi altra estensione solleva un errore prima che venga fatta una richiesta.

| Opzione | Tipo | Obbligatoria | Descrizione |
|--------|------|----------|-------------|
| `saveTo` | `string` | -- | Percorso locale su cui scrivere il Markdown in streaming. |
| `outputFilename` | `string` | -- | Sovrascrive il nome file generato. |

Se vuoi che anche il chunking sia fatto per te, passa gli stessi file a [`client.v2.ingestFiles`](#ingest).

---

### `convertToPdf`

Converte in PDF qualsiasi documento, immagine, ebook, file web o file di testo semplice supportato.

```ts
// docx in pdf
await client.convertToPdf("contract.docx", { saveTo: "contract.pdf" });

// html in pdf con geometria di pagina completa
await client.convertToPdf("invoice.html", {
    pdfOptions: { pageSize: "A4", margins: { top: 15, bottom: 15, left: 15, right: 15 } },
    saveTo: "invoice.pdf",
});

// pdf in pdf in scala di grigi (passthrough)
await client.convertToPdf("scan.pdf", {
    pdfOptions: { grayscale: true },
    saveTo: "scan-gray.pdf",
});
```

**Input supportati (36):** `.bmp`, `.csv`, `.doc`, `.docx`, `.epub`, `.gif`, `.heic`, `.heif`, `.htm`, `.html`, `.jpeg`, `.jpg`, `.markdown`, `.md`, `.mdown`, `.mkd`, `.numbers`, `.odp`, `.ods`, `.odt`, `.ots`, `.pages`, `.pdf`, `.png`, `.ppt`, `.pptx`, `.rtf`, `.svg`, `.text`, `.tif`, `.tiff`, `.txt`, `.webp`, `.xhtml`, `.xls`, `.xlsx`.

Un input `.pdf` viene accettato e restituito così com'è, quindi con `pdfOptions: { grayscale: true }` questo metodo funziona anche come percorso di normalizzazione PDF. Anche EPUB viene gestito qui, dato che non ha una coppia di conversione documenti dedicata. Qualsiasi altra estensione solleva un errore prima che venga fatta una richiesta.

<div class="alert alert-warning">
<strong>La geometria dipende dall'input.</strong> La geometria di pagina completa (dimensione pagina, larghezza e altezza pagina, orientamento, margini, scala, intestazione, piè di pagina) viene rispettata per input HTML (<code>.html</code>, <code>.htm</code>, <code>.xhtml</code>), Markdown, testo semplice, EPUB, immagini e SVG. Gli input Office, ODF, iWork, RTF e CSV, oltre al passthrough PDF, supportano solo <code>grayscale</code> e restituiscono <code>400</code> se viene impostata un'opzione di geometria esplicita. <code>grayscale</code> è invece rispettato per ogni tipo di input.
</div>

| Opzione | Tipo | Obbligatoria | Descrizione |
|--------|------|----------|-------------|
| `saveTo` | `string` | -- | Percorso locale su cui scrivere il PDF in streaming. |
| `outputFilename` | `string` | -- | Sovrascrive il nome file generato. `.pdf` viene aggiunto se manca. |
| `pdfOptions` | `PdfOptions` | -- | Impostazioni di pagina. Vedi l'avvertenza qui sopra su quali input rispettano la geometria. |

---

### `getJobStatus`

Interroga lo stato di un job asincrono o recuperato.

```ts
const status = await client.getJobStatus("job_abc123");

if (status.status === "success") {
    console.log(status.presignedUrl);
} else if (status.status === "failed") {
    console.error(status.error);
}
```

Restituisce `{ status: "processing" | "success" | "failed", presignedUrl?, objectKey?, error? }`.

<div class="alert alert-info">
<strong>Di solito non serve chiamarlo direttamente.</strong> L'SDK esegue il polling in automatico quando una richiesta sincrona restituisce 5xx. Vedi <a href="#timeout-recovery">Recupero dei timeout</a> più sotto.
</div>

---

### Helper batch per interi siti

`convertWebsiteToPdf` e `convertWebsiteToScreenshot` individuano le pagine di un sito, le accodano tutte e raccolgono gli output in un unico ZIP. Entrambi restituiscono subito un `BatchSubmission`; interroga con `getBatchStatus` oppure bloccati con `waitForBatch`. Le opzioni condivise sono `crawlMode` (`"auto"`, `"sitemap"` o `"full"`), `includePatterns`, `excludePatterns`, `notificationEmail` e `callbackUrl`; `convertWebsiteToPdf` aggiunge `singlePage` e `pdfOptions`.

```ts
const batch = await client.convertWebsiteToPdf("https://example.com", {
    crawlMode: "sitemap",
    excludePatterns: ["/tag/"],
});

const done = await client.waitForBatch(batch.batchId, { saveTo: "site.zip" });
console.log(done.status, done.completed, done.failed, done.zipDownloadUrl);
```

`waitForBatch` accetta `intervalMs` (default `5_000`), `timeoutMs` (default `1_800_000`, trenta minuti) e `saveTo`. Solleva `APIError(504, ...)` se la scadenza viene superata. Vedi la [panoramica degli endpoint](/it/docs/endpoints.md) per la superficie REST.

---

## Web intelligence (V2)

Tutto ciò che sta sotto `client.v2` restituisce dati di cui un agente può fidarsi, perché ogni render V2 porta con sé un punteggio `renderQuality` da 0.0 a 1.0. Una pagina bloccata, una sfida anti-bot, un muro di login, una pagina di errore HTTP, un soft 404 o uno shell SPA vuoto tornano con un punteggio basso più `deductions` e `warnings` con un nome, così vengono segnalati invece di essere scambiati per contenuto reale. Il contenuto viene comunque restituito; sta a te decidere cosa farne. Punteggi sotto circa 0.40 significano che il render non è riuscito in alcun senso utile.

Ventitré metodi su sei capacità:

| Metodo | Endpoint | Restituisce |
|--------|----------|---------|
| `v2.perceive(url, options?)` | `POST /v2/perceive` | `PerceiveResult` |
| `v2.perceiveDirect(url, options?)` | `POST /v2/perceive` | `PerceiveDirectResult` |
| `v2.getPerceiveOperation(operationId)` | `GET /v2/perceive/{operationId}` | `PerceiveResult` |
| `v2.downloadPerceiveArtifact(operationId, output?)` | `GET /v2/perceive/{operationId}` | `PerceiveDirectResult` |
| `v2.perceiveBatch(urls, options?)` | `POST /v2/perceive/batch` | `PerceiveBatchResult` |
| `v2.getPerceiveBatch(jobId)` | `GET /v2/perceive/batch/{jobId}` | `PerceiveBatchResult` |
| `v2.discover(url, options?)` | `POST /v2/discover` | `DiscoverResult` |
| `v2.lookup(query, options?)` | `POST /v2/lookup` | `LookupResult` |
| `v2.distill(options)` | `POST /v2/distill` | `DistillResult` |
| `v2.ingest(options)` | `POST /v2/ingest` | `IngestJob` |
| `v2.ingestFiles(files, options?)` | `POST /v2/ingest/files` | `IngestJob` |
| `v2.listIngestJobs(options?)` | `GET /v2/ingest` | `IngestJobList` |
| `v2.getIngestJob(jobId)` | `GET /v2/ingest/{jobId}` | `IngestJob` |
| `v2.cancelIngestJob(jobId)` | `DELETE /v2/ingest/{jobId}` | `IngestJob` |
| `v2.retryIngestWebhook(jobId)` | `POST /v2/ingest/{jobId}/retry-webhook` | `WebhookRetryResult` |
| `v2.getWebhookSecret()` | `GET /v2/ingest/webhook-secret` | `WebhookSecret` |
| `v2.rotateWebhookSecret()` | `POST /v2/ingest/webhook-secret/rotate` | `WebhookSecret` |
| `v2.createWatcher(url, options?)` | `POST /v2/watch` | `Watcher` |
| `v2.listWatchers(options?)` | `GET /v2/watch` | `WatcherList` |
| `v2.getWatcher(watcherId)` | `GET /v2/watch/{watcherId}` | `Watcher` |
| `v2.getWatcherSnapshots(watcherId, options?)` | `GET /v2/watch/{watcherId}/snapshots` | `WatcherSnapshotList` |
| `v2.updateWatcher(watcherId, updates)` | `PATCH /v2/watch/{watcherId}` | `Watcher` |
| `v2.deleteWatcher(watcherId)` | `DELETE /v2/watch/{watcherId}` | `Watcher` |

Le opzioni sono in camelCase sulla superficie dell'SDK e vengono serializzate nel formato wire snake_case dell'API; le risposte vengono rimappate in camelCase. I tuoi payload (schemi di estrazione, dati estratti, campi tracciati, voci di diff) passano invariati.

---

### Perceive

Esegue il rendering di un URL negli artefatti che richiedi: Markdown, HTML pulito o grezzo, uno screenshot del viewport o a pagina intera, un PDF, un elenco di link, un elenco di immagini o dati strutturati. `perceive` è sincrono e restituisce l'operazione completata con URL firmati per gli artefatti validi 15 minuti. Riferimento completo: [Perceive](/it/docs/endpoints/perceive.md).

```ts
const op = await client.v2.perceive("https://example.com", {
    outputs: ["markdown", "screenshot", "structured"],
    extract: ["tables", "metadata"],
    onlyMainContent: true,
    waitFor: "css:.article-body",
    viewport: { width: 1440, height: 900 },
});

console.log(op.renderQuality);        // da 0.0 a 1.0
console.log(op.deductions);           // ad es. { http_error: 0.7 }
console.log(op.outputs.markdown.url); // URL firmato, 15 minuti
console.log(op.structured);

if ((op.renderQuality ?? 0) < 0.4) {
    console.warn("Bad read, do not feed this to the model:", op.warnings);
}

// Rifirma gli URL degli artefatti più tardi senza rieseguire il rendering:
const again = await client.v2.getPerceiveOperation(op.operationId);
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `outputs` | `PerceiveOutputName[]` | `["markdown", "structured"]` | Uno o più tra `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `extract` | `PerceiveExtractName[]` | -- | Target euristici: `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `onlyMainContent` | `boolean` | `true` | Rimuove navigazione, header, footer e banner dei cookie dall'output Markdown, protetto da una verifica di fedeltà. `false` restituisce la pagina intera intatta. |
| `schema` | `Record<string, unknown>` | -- | Schema JSON per l'estrazione strutturata sul tier LLM. |
| `waitFor` | `string` | -- | Selettore CSS (facoltativamente `"css:..."`) o `"js:<expr>"` da attendere prima della cattura. |
| `waitTimeoutMs` | `number` | `30000` | Da 0 a 60000. |
| `jsCode` | `string` | -- | JavaScript eseguito dopo la navigazione. Massimo 20000 caratteri. |
| `viewport` | `{ width?, height? }` | `1920 x 1080` | Larghezza da 320 a 3840, altezza da 240 a 2160. |
| `headers` | `Record<string, string>` | -- | Header di richiesta aggiuntivi. |
| `cookies` | `BrowserCookie[]` | -- | Cookie iniettati prima del rendering. Ognuno richiede `name`, `value` e o `domain` o `url`. |
| `auth` | `{ username, password }` | -- | HTTP Basic Auth. |
| `cacheMode` | `"enabled" \| "bypass" \| "refresh"` | `"enabled"` | Cache di un'ora. `bypass` la salta, `refresh` forza un nuovo rendering. |
| `pdfOptions` | `PdfOptions` | -- | Ha senso solo quando `outputs` include `"pdf"`. Vedi [Opzioni PDF](#opzioni-pdf). |
| `blockResources` | `PerceiveResourceType[]` | -- | Tipi di risorsa che il browser non deve caricare, per esempio `["image", "font", "media"]`. |
| `respectRobots` | `boolean` | -- | Rispetta le regole robots del sito. |
| `mobile` | `boolean` | -- | Esegue il rendering con un profilo mobile. |
| `directDownload` | `boolean` | -- | Solo per `perceive`. Preferisci `perceiveDirect`, che la imposta al posto tuo. |

<div class="alert alert-warning">
<strong>Tre opzioni sono dichiarate ma non ancora attive.</strong> <code>proxyUrl</code>, <code>geolocation</code> e <code>actionChain</code> sono tipizzate su <code>PerceiveOptions</code> ma al momento vengono rifiutate lato server con <code>422</code>. Sono riservate, non utilizzabili.
</div>

**Download diretto.** `perceiveDirect` salta il giro di andata e ritorno con l'URL firmato: il corpo della risposta HTTP contiene i byte dell'artefatto e i metadati viaggiano negli header. Richiede esattamente un output che produca un artefatto, e l'SDK solleva un errore in locale prima di inviare se non è così (`"structured"` può viaggiare insieme, ma resta inline lato server e non viene restituito).

```ts
import { writeFile } from "node:fs/promises";

const direct = await client.v2.perceiveDirect("https://example.com", { outputs: ["markdown"] });
console.log(direct.contentType, direct.renderQuality, direct.sourceStatusCode);
await writeFile(direct.filename ?? "page.md", direct.content);

// Riscarica come byte grezzi un artefatto salvato da un'operazione precedente.
// `output` si può omettere quando l'operazione ha prodotto esattamente un artefatto.
const raw = await client.v2.downloadPerceiveArtifact(op.operationId, "markdown");
```

`downloadPerceiveArtifact` restituisce `410` quando l'artefatto salvato è scaduto, e `400` (elencando gli output disponibili) se l'operazione ha prodotto più di un artefatto e hai omesso `output`.

**Batch.** `perceiveBatch` accetta fino a 1000 URL con un unico blocco di opzioni condivise. I batch piccoli girano inline e tornano già completati; quelli più grandi restituiscono lo stato `"queued"`, quindi interroga `getPerceiveBatch` con il `jobId` restituito.

```ts
const batch = await client.v2.perceiveBatch(["https://example.com/a", "https://example.com/b"], {
    outputs: ["markdown"],
    outputMode: "zip",
});

let job = await client.v2.getPerceiveBatch(batch.jobId);
while (job.status === "queued" || job.status === "processing") {
    await new Promise((r) => setTimeout(r, 3000));
    job = await client.v2.getPerceiveBatch(batch.jobId);
}
console.log(job.completed, job.failed, job.zip?.url);
```

`outputMode` è `"manifest"` (predefinito, una voce per URL in `items`) oppure `"zip"` (tutti gli artefatti riusciti raccolti insieme una volta finito il job). L'endpoint batch rifiuta `directDownload`; usa invece `outputMode: "zip"`.

---

### Discover

Elenca gli URL di un sito senza eseguire alcun rendering. Non entra in gioco nessun browser, quindi è veloce ed economico rispetto al percepire ogni singola pagina. Riferimento completo: [Discover](/it/docs/coming-soon/discover.md).

```ts
const found = await client.v2.discover("https://example.com", {
    mode: "hybrid",
    maxUrls: 200,
    maxDepth: 3,
    excludePatterns: ["/tag/", "/author/"],
    sameDomainOnly: true,
});

console.log(found.total, found.truncated, found.sources); // ad es. { sitemap: 42, crawl: 30 }
for (const url of found.urls) console.log(url);
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `mode` | `"sitemap" \| "crawl" \| "hybrid"` | `"hybrid"` | Solo sitemap, solo crawl HTTP, o entrambi. |
| `maxUrls` | `number` | `100` | Da 1 a 1000. `truncated` è `true` quando esistevano più URL di quanti questo limite ne permettesse. |
| `maxDepth` | `number` | `2` | Da 1 a 5. Profondità del crawl a partire dall'URL di partenza. |
| `includePatterns` | `string[]` | -- | Allowlist di espressioni regolari, massimo 50 voci. |
| `excludePatterns` | `string[]` | -- | Denylist di espressioni regolari applicata dopo `includePatterns`, massimo 50 voci. |
| `sameDomainOnly` | `boolean` | `true` | Mantiene il crawl sul dominio di partenza. |
| `respectRobots` | `boolean` | -- | Rispetta le regole robots del sito. `robotsRespected` nel risultato riporta cosa è successo. |

---

### Lookup

Esegue una ricerca web categorizzata e, facoltativamente, percepisce in automatico i primi risultati, così ogni hit porta con sé il proprio `PerceiveResult` completo inline. Riferimento completo: [Lookup](/it/docs/coming-soon/lookup.md).

```ts
const search = await client.v2.lookup("best static site generators", {
    category: "web",
    numResults: 10,
    country: "us",
    locale: "en",
    timeFilter: "month",
    perceiveTop: 3,
});

for (const hit of search.results) {
    console.log(hit.position, hit.title, hit.url);
    if (hit.perceive) {
        console.log("  quality:", hit.perceive.renderQuality);
        console.log("  markdown:", hit.perceive.outputs.markdown?.url);
    }
}
console.log(search.answerBox, search.knowledgeGraph);
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `category` | `"web" \| "news" \| "images" \| "scholar" \| "patents" \| "maps"` | `"web"` | Verticale di ricerca. |
| `country` | `string` | -- | Codice paese `gl` di Google, per esempio `"us"` o `"in"`. |
| `locale` | `string` | -- | Lingua dell'interfaccia `hl` di Google, per esempio `"en"`. |
| `timeFilter` | `"hour" \| "day" \| "week" \| "month" \| "year"` | -- | Finestra temporale di recency. |
| `numResults` | `number` | `10` | Da 1 a 100. |
| `page` | `number` | `1` | Da 1 a 10. |
| `location` | `string` | -- | Località in testo libero, per esempio `"Austin, Texas"`. |
| `autocorrect` | `boolean` | `true` | Lascia che il provider corregga i refusi evidenti. |
| `perceiveTop` | `number` | `0` | Da 0 a 10. Esegue il rendering automatico dei primi N URL dei risultati; ognuno è un render completo del browser. |

`perceiveTop` nel risultato riporta quanti risultati sono stati effettivamente percepiti, che possono essere meno di quanti ne avevi chiesti, e `perceiveOperationIds` ti dà gli id delle operazioni per rifirmare gli URL più tardi.

---

### Distill

Estrazione strutturata guidata da uno schema. Dagli una forma e un insieme di URL (o un sito da scoprire prima) e restituisce record conformi a quella forma. Un `cssSchema` facoltativo risponde a tutto ciò che può a partire dai selettori, prima che qualcosa venga escalato al tier LLM. Riferimento completo: [Distill](/it/docs/coming-soon/distill.md).

```ts
const extraction = await client.v2.distill({
    urls: ["https://example.com/pricing"],
    schema: { plans: "list of plan names with monthly prices" },
    cssSchema: {
        baseSelector: ".plan-card",
        fields: [
            { name: "name", type: "text", selector: "h3" },
            { name: "price", type: "text", selector: ".price" },
            { name: "url", type: "attribute", selector: "a", attribute: "href" },
        ],
    },
});

const first = extraction.results[0];
console.log(first.data);
console.log(first.extractionTier);  // "css" | "llm" | "mixed" | "none"
console.log(first.fieldsFromCss, first.fieldsFromLlm, first.renderQuality);
```

Passa esattamente uno tra `urls` e `discoverFrom`; l'SDK solleva un errore in locale se li passi entrambi o nessuno dei due, e lo solleva anche se `schema` manca o non è un oggetto.

```ts
// Scopri prima un sito, poi distilla ogni pagina trovata.
await client.v2.distill({
    discoverFrom: { url: "https://example.com", mode: "sitemap", maxPages: 10 },
    schema: { title: "page title", summary: "one-line summary" },
});
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `urls` | `string[]` | -- | URL espliciti da distillare, massimo 50. Mutuamente esclusivo con `discoverFrom`. |
| `discoverFrom` | `{ url, mode?, maxPages? }` | -- | Scopre prima, poi distilla. `maxPages` va da 1 a 50, default 10, e limita sia la scoperta sia la distillazione. |
| `schema` | `Record<string, unknown>` | obbligatorio | Un oggetto JSON-Schema (`{ type: "object", properties: {...} }`) oppure una mappa piatta `{ campo: descrizione }`. |
| `cssSchema` | `CssSchema` | -- | Passaggio gratuito sui selettori eseguito prima di qualsiasi escalation all'LLM. |
| `waitFor` | `string` | -- | Selettore CSS o `"js:<expr>"` da attendere. |
| `waitTimeoutMs` | `number` | `30000` | Da 0 a 60000. |
| `headers` | `Record<string, string>` | -- | Header di richiesta aggiuntivi. |
| `cookies` | `BrowserCookie[]` | -- | Cookie iniettati prima del rendering. |
| `respectRobots` | `boolean` | -- | Rispetta le regole robots del sito. |

Un `CssSchema` ha un `baseSelector` (il contenitore ripetuto, un record per ogni corrispondenza), un elenco `fields`, un `name` facoltativo e un `targetField` facoltativo che indica la proprietà dello schema di output che i record vanno a riempire. Ogni campo è `{ name, type, selector?, attribute?, pattern?, default?, transform?, fields? }` dove `type` è uno tra `text`, `attribute`, `html`, `regex`, `nested`, `list`, `nested_list`. `attribute` è obbligatorio per i campi `attribute`, `pattern` per i campi `regex`, e un array `fields` non vuoto per i tipi annidati (profondità massima 5).

---

### Ingest

Trasforma un sito, o una pila di documenti caricati, in JSONL suddiviso in chunk e pronto per RAG. Ingest è sempre asincrono: entrambi i punti di ingresso restituiscono un `IngestJob` in coda, e tu lo interroghi oppure configuri un webhook. Riferimento completo: [Ingest](/it/docs/endpoints/ingest.md).

```ts
// Da un sito.
const job = await client.v2.ingest({
    mode: "sitemap",
    url: "https://docs.example.com",
    maxPages: 100,
    chunk: { maxWords: 512, sentenceOverlap: 1 },
    webhookUrl: "https://my.app/hooks/enconvert",
});

// Da file caricati: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD,
// e documenti office legacy o ODF.
const fileJob = await client.v2.ingestFiles(["handbook.pdf", "notes.docx"], {
    chunk: { maxWords: 512, sentenceOverlap: 1 },
});

// Interroga entrambi allo stesso modo. Stati non terminali: queued, discovering, processing.
let status = await client.v2.getIngestJob(job.jobId);
while (!["completed", "failed", "canceled"].includes(status.status)) {
    await new Promise((r) => setTimeout(r, 5000));
    status = await client.v2.getIngestJob(job.jobId);
}
console.log(status.totalChunks, status.outputUrl, status.errorMessage);

const page = await client.v2.listIngestJobs({ limit: 20, skip: 0 });
console.log(page.jobs.length, page.hasMore);

await client.v2.cancelIngestJob(job.jobId); // idempotente
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `mode` | `"urls" \| "sitemap" \| "crawl"` | `"urls"` | `"urls"` richiede `urls` e rifiuta `url`. `"sitemap"` e `"crawl"` richiedono un `url` di partenza e rifiutano `urls`. Entrambe le regole vengono applicate in locale prima della richiesta. L'unione `IngestMode` include anche `"files"`, che è ciò che `ingestFiles` riporta sul suo job; non passarlo qui. |
| `url` | `string` | -- | URL di partenza per `sitemap` e `crawl`. |
| `urls` | `string[]` | -- | URL espliciti per la modalità `"urls"`, massimo 1000. |
| `maxPages` | `number` | `50` | Limite di scoperta per `sitemap` e `crawl`, da 1 a 1000. |
| `maxDepth` | `number` | `2` | Da 1 a 5. |
| `sameDomainOnly` | `boolean` | `true` | Mantiene il crawl sul dominio di partenza. |
| `includePatterns` / `excludePatterns` | `string[]` | -- | Allowlist e denylist di espressioni regolari. |
| `respectRobots` | `boolean` | -- | Rispetta le regole robots del sito. |
| `waitFor` / `waitTimeoutMs` | `string` / `number` | -- / `30000` | Attesa di rendering per singola pagina. |
| `chunk` | `{ maxWords?, sentenceOverlap? }` | `512` / `1` | `maxWords` va da 32 a 4000, `sentenceOverlap` da 0 a 10. |
| `webhookUrl` | `string` | -- | Webhook di completamento, firmato in HMAC. |

`ingestFiles` accetta un `FileInput[]`, ossia stringhe di percorso, `Uint8Array` / `Buffer` oppure oggetti `{ data, filename, contentType? }`, in qualsiasi combinazione. Prende solo `chunk` e `webhookUrl`, e solleva un errore in locale su un elenco vuoto.

**Firma dei webhook.** I webhook di completamento sono firmati in HMAC. Recupera il segreto (viene creato alla prima chiamata) per verificare le consegne, ruotalo quando serve e riconsegna un webhook che il tuo endpoint ha perso.

```ts
const secret = await client.v2.getWebhookSecret();
console.log(secret.signatureHeader, secret.timestampHeader);
console.log(secret.signatureScheme, secret.replayToleranceSeconds);

// La rotazione invalida immediatamente le firme fatte con il segreto precedente.
const rotated = await client.v2.rotateWebhookSecret();

// Riconsegna il webhook di un job completato.
const retry = await client.v2.retryIngestWebhook(job.jobId);
console.log(retry.delivered, retry.attempts, retry.statusCode, retry.detail);
```

`retryIngestWebhook` restituisce `409` quando il job non è completato e `400` quando il job non ha alcun webhook configurato.

---

### Watch

Crea un watcher che riesegue il rendering di un URL a cadenza fissa e ti avvisa quando la pagina cambia, via email, via webhook o entrambi. Riferimento completo: [Watch](/it/docs/coming-soon/watch.md).

```ts
const watcher = await client.v2.createWatcher("https://example.com/pricing", {
    frequencyMinutes: 60,
    diffMode: "auto",
    webhookUrl: "https://my.app/hooks/changes",
    notifyEmail: true,
});
console.log(watcher.watcherId, watcher.nextCheckAt);

const list = await client.v2.listWatchers({ limit: 20 });
const one = await client.v2.getWatcher(watcher.watcherId);

const history = await client.v2.getWatcherSnapshots(watcher.watcherId, { limit: 10 });
for (const snap of history.snapshots) {
    console.log(snap.checkedAt, snap.hasChanges, snap.similarity, snap.changeCount);
}

await client.v2.updateWatcher(watcher.watcherId, { status: "paused" });
await client.v2.updateWatcher(watcher.watcherId, { webhookUrl: "" }); // azzera il webhook
await client.v2.deleteWatcher(watcher.watcherId);                     // soft delete, idempotente
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `frequencyMinutes` | `number` | `60` | Minuti tra un controllo e l'altro, da 60 a 43200. Il limite minimo di un'ora è rigido. |
| `diffMode` | `"auto" \| "text" \| "structured" \| "tables" \| "metadata"` | `"auto"` | `"auto"` lascia scegliere al motore di diff in base al tipo di contenuto. |
| `trackFields` | `Record<string, unknown>` | -- | Sottoinsieme di campi o selettori per restringere il diff. |
| `webhookUrl` | `string` | -- | Webhook di notifica dei cambiamenti, firmato in HMAC. |
| `notifyEmail` | `boolean` | `true` | Invia un'email al proprietario del progetto quando ci sono cambiamenti. |

`updateWatcher` accetta gli stessi campi più `status` (`"active"` o `"paused"`) e ne richiede almeno uno; l'SDK solleva un errore in locale su un aggiornamento vuoto. Passare `webhookUrl: ""` in modo esplicito azzera il webhook. L'eliminazione è una soft delete: `deleteWatcher` restituisce il watcher marcato come eliminato con stato `"deleted"`, e un watcher eliminato risulta `404` da `getWatcher`.

Ogni snapshot porta con sé `checkedAt`, `hasChanges`, `similarity` (da 0.0 a 1.0 rispetto alla cattura precedente), `renderQuality`, `changeCount` e un array `changes`.

<div class="alert alert-warning">
<strong>I diff degli snapshot contengono contenuto di pagina non attendibile.</strong> Le voci in <code>snapshot.changes</code> arrivano direttamente dalla pagina sorvegliata. Applica l'escape prima di renderizzarle in HTML o di scriverle in un visualizzatore di log.
</div>

---

## Opzioni PDF

Si passano tramite il campo `pdfOptions` su `convertUrlToPdf`, `convertDocument`, `convertToPdf`, `convertWebsiteToPdf` e `client.v2.perceive` (quando `outputs` include `"pdf"`).

```ts
const result = await client.convertUrlToPdf("https://example.com", {
    pdfOptions: {
        pageSize: "A4",
        orientation: "landscape",
        margins: { top: 10, bottom: 10, left: 15, right: 15 },
        scale: 0.9,
        grayscale: false,
    },
    saveTo: "report.pdf",
});
```

| Campo | Tipo | Descrizione |
|-------|------|-------------|
| `pageSize` | `string` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"`, ecc. |
| `pageWidth` / `pageHeight` | `number` | Geometria di pagina esplicita, in alternativa a `pageSize`. |
| `orientation` | `"portrait" \| "landscape"` | Il valore predefinito è verticale. |
| `margins` | `{ top, bottom, left, right }` (mm) | Tutti e quattro sono facoltativi. |
| `scale` | `number` | Scala di rendering, ad es. `0.9` per il 90%. |
| `grayscale` | `boolean` | Post-elabora il PDF con Ghostscript convertendolo in scala di grigi. |
| `header` | `PdfHeaderFooter` | `{ content?, height? }`. `content` è limitato a 2000 caratteri. |
| `footer` | `PdfHeaderFooter` | Stessa forma di `header`. |

Ogni parametro è descritto per esteso in [Job sync e async](/it/docs/concepts/sync-and-async.md).

## Gestione degli errori

Gli errori sono classi di eccezione tipizzate che puoi riconoscere con `instanceof`. La stessa gerarchia copre sia i metodi di conversione sia `client.v2`.

```ts
import {
    Enconvert,
    APIError,
    AuthenticationError,
    QuotaError,
    RateLimitError,
} from "@enconvert/node-sdk";

try {
    await client.v2.perceive("https://example.com", { outputs: ["markdown"] });
} catch (e) {
    if (e instanceof AuthenticationError) {
        console.error("Invalid API key. Check ENCONVERT_API_KEY.");
    } else if (e instanceof QuotaError) {
        console.error("Request rejected with 402.");
    } else if (e instanceof RateLimitError) {
        console.error("Too many requests. Back off and retry.");
    } else if (e instanceof APIError) {
        console.error(`API error [${e.statusCode}]: ${e.message}`);
    } else {
        throw e;
    }
}
```

| Classe | Sollevata su | Codice di stato |
|-------|-----------|-------------|
| `AuthenticationError` | Chiave non valida, mancante o revocata | `401`, `403` (entrambi riportano `statusCode` `401`) |
| `QuotaError` | Sollevata su HTTP 402 | `402` |
| `RateLimitError` | Troppe richieste | `429` |
| `APIError` | Qualsiasi altro 4xx / 5xx | il codice effettivo |
| `EnconvertError` | Classe base di tutte le precedenti | -- |

`QuotaError` e `RateLimitError` estendono entrambe `APIError`, che estende `EnconvertError`, quindi ordina i tuoi controlli `instanceof` dal più specifico al meno specifico. Ogni `APIError` porta con sé un campo `statusCode`.

Alcuni errori non raggiungono mai la rete: un'estensione di file non supportata, una chiamata `distill` con sia `urls` sia `discoverFrom`, una chiamata `ingest` in cui modalità e argomenti non concordano, una chiamata `perceiveDirect` con più di un output di artefatto, o una chiamata `updateWatcher` senza campi. Questi casi sollevano un semplice `Error` in locale, così trovi lo sbaglio in fase di sviluppo.

La mappa completa dei messaggi di errore è nel riferimento [Codici di errore](/it/docs/reference/errors.md).

---

## Recupero dei timeout

Le conversioni URL-to-PDF lunghe o quelle di documenti di grandi dimensioni possono superare il limite di timeout del reverse-proxy di 60-120 secondi, anche quando la conversione alla fine riesce sul server. L'SDK gestisce la cosa in modo trasparente sui metodi di conversione V1:

1. Prima di ogni richiesta, l'SDK genera un UUID e lo invia come `job_id` nel corpo della richiesta.
2. Se la richiesta originale restituisce 5xx, l'SDK passa silenziosamente al polling di `GET /v1/convert/status/{job_id}` ogni 3 secondi.
3. Appena il job risulta registrato come `success`, l'SDK restituisce il risultato. Appena risulta registrato come `failed`, l'SDK solleva `APIError`.
4. La scadenza del polling è di 5 minuti. Se viene superata, l'SDK solleva `APIError(504, "Conversion timed out")`.

Non devi scrivere codice per tutto questo, funziona e basta. Imposta `timeout` sul costruttore se vuoi limitare la richiesta iniziale.

V2 usa oggetti job espliciti invece del recupero implicito: `perceiveBatch` e `ingest` restituiscono un id che interroghi con `getPerceiveBatch` e `getIngestJob`, e `ingest` può chiamare un webhook al posto tuo.

---

## Configurazione

```ts
const client = new Enconvert({
    apiKey: process.env.ENCONVERT_API_KEY!,
    timeout: 300_000, // ms, default 5 minuti
    baseUrl: "https://api.enconvert.com", // override per gateway self-hosted
});
```

| Opzione | Tipo | Default | Descrizione |
|--------|------|---------|-------------|
| `apiKey` | `string` | -- (obbligatoria) | Chiave API privata (`sk_...`). Il costruttore solleva subito un errore se manca. |
| `timeout` | `number` | `300_000` | Timeout della richiesta in ms. Interrompe la `fetch` sottostante tramite `AbortController`. |
| `baseUrl` | `string` | `https://api.enconvert.com` | URL base dell'API. Le barre finali vengono rimosse. |

La chiave viaggia come header `X-API-Key` su ogni richiesta, sia V1 sia V2. `client.v2` viene costruito per te e condivide chiave, URL base e timeout del client, quindi non c'è nulla di ulteriore da configurare.

<div class="alert alert-warning">
<strong>Non scrivere mai la chiave API hardcoded.</strong> Leggila da una variabile d'ambiente o dal tuo secret manager. Chiunque ottenga la tua chiave privata può eseguire conversioni e operazioni V2 sul tuo account. Ruota le chiavi dalla <a href="/it/dashboard">dashboard</a>.
</div>

---

## Forma del risultato

Ogni metodo di conversione restituisce un `ConversionResult`:

```ts
interface ConversionResult {
    presignedUrl: string;          // URL firmato per scaricare l'output (1 ora)
    objectKey: string;             // chiave dell'oggetto nello storage
    filename: string;              // nome file lato server
    fileSize?: number;             // byte
    conversionTimeSeconds?: number;
    jobId?: string;                // presente quando il recupero timeout ha fatto polling
}
```

L'URL pre-firmato è valido per un'ora. Se ti serve un accesso permanente, scarica il file (usa `saveTo`, oppure recupera tu stesso l'URL) e archivialo nel tuo bucket.

I risultati V2 hanno una forma diversa. Un `PerceiveResult` porta con sé `operationId`, `status`, `url`, `urlFinal`, `contentHash`, `renderQuality`, `statusCode`, `deductions`, `cacheHit`, una mappa `outputs` indicizzata per nome di output, `structured`, `extractionTier`, `tokens`, `costCents`, `durationMs`, `optionsEcho`, `error` e `warnings`. Ogni voce in `outputs` è un `V2OutputArtifact` fatto di `{ url?, objectKey, sizeBytes, contentType, expiresIn }`, dove `expiresIn` è espresso in secondi e vale `900` per impostazione predefinita. Gli URL degli artefatti V2 durano quindi 15 minuti invece di un'ora, e vengono rifirmati a ogni lettura, quindi chiamare di nuovo `getPerceiveOperation(operationId)` ti dà link freschi senza rieseguire il rendering della pagina.

---

## TypeScript

Le definizioni di tipo sono incluse nel pacchetto, quindi non serve installare alcun `@types/...`. Il pacchetto è pubblicato in doppia forma (ESM + CJS) con `exports`, `types` e `.d.ts` / `.d.cts` corretti, così funziona con qualsiasi modalità di risoluzione dei moduli di Node.

```ts
import type {
    ClientOptions,
    CompressImageOptions,
    ConversionResult,
    ConvertDocumentOptions,
    ConvertImageOptions,
    ConvertToMarkdownOptions,
    ConvertToPdfOptions,
    FileInput,
    JobStatus,
    PdfOptions,
    UrlToMarkdownOptions,
    UrlToPdfOptions,
    UrlToScreenshotOptions,
    // I tipi V2 arrivano dallo stesso entry point.
    DiscoverOptions, DiscoverResult,
    DistillOptions, DistillResult,
    IngestJob, IngestOptions,
    LookupOptions, LookupResult,
    PerceiveOptions, PerceiveResult, PerceiveOutputName,
    PerceiveBatchResult, PerceiveDirectResult,
    Watcher, WatcherSnapshotList,
} from "@enconvert/node-sdk";
```

Anche la classe `EnconvertV2` è esportata, se vuoi tipizzare il parametro di una funzione come namespace V2.

---

## Aggiornamento

Il pacchetto include una piccola CLI, `enconvert-sdk`, per mantenersi aggiornato.

```bash
npx enconvert-sdk upgrade
```

```bash
npx enconvert-sdk upgrade --dry-run
```

```bash
npx enconvert-sdk version
```

`upgrade` rileva npm, pnpm, yarn o bun dal package manager in uso e stampa sempre il comando di installazione esatto prima di eseguirlo, così non succede nulla al tuo lockfile senza che tu lo veda. `--dry-run` stampa quel comando e si ferma. `version` riporta la versione dell'SDK installata.

---

## Sorgente e segnalazioni

- **npm:** [@enconvert/node-sdk](https://www.npmjs.com/package/@enconvert/node-sdk)
- **GitHub:** [enconvert/node-sdk](https://github.com/enconvert/node-sdk)
- **Licenza:** MIT
- **Altri linguaggi:** [Tutti gli SDK](/it/docs/guides/integrations/sdks.md)

---

## Domande frequenti

### Come converto file in Node.js con un pacchetto npm?

Installa `@enconvert/node-sdk`, crea un client con la tua chiave API (`new Enconvert({ apiKey: process.env.ENCONVERT_API_KEY! })`) e chiama un metodo tipizzato come `convertUrlToPdf`, `convertImage` o `convertDocument`. Passa `saveTo` per scrivere il risultato direttamente su disco in streaming.

### Come estraggo una pagina web in Markdown pulito con Node.js?

Chiama `client.v2.perceive(url, { outputs: ["markdown"] })`. Ottieni un URL firmato valido 15 minuti verso il Markdown in `op.outputs.markdown.url`, più un punteggio `renderQuality` per la lettura. Se vuoi i byte direttamente invece di un URL, chiama `client.v2.perceiveDirect(url, { outputs: ["markdown"] })` e leggi `result.content`.

### Cos'è renderQuality e perché è importante?

`renderQuality` è un punteggio da 0.0 a 1.0 allegato a ogni render V2. Una sfida anti-bot, un muro di login, una pagina di errore HTTP, un soft 404 o uno shell SPA vuoto ottengono tutti un punteggio basso e tornano con `deductions` e `warnings` con un nome, così una lettura sbagliata viene segnalata invece di entrare in silenzio nel contesto del tuo agente come se fosse la pagina vera. Punteggi sotto circa 0.40 significano che il render è fallito nei fatti, anche se la richiesta ha restituito 200.

### Come converto HEIC in WebP con Node.js?

Chiama `convertImage` con il file HEIC (un percorso o un oggetto buffer `{ data, filename }`) e `outputFormat: "webp"`. L'SDK converte tra `jpeg`, `png`, `svg`, `heic` e `webp`; il formato di input viene rilevato dall'estensione del nome file.

### Come comprimo un'immagine in Node.js senza cambiarne il formato?

Chiama `compressImage` con un file `.png`, `.jpg`, `.jpeg` o `.webp`. L'output conserva formato ed estensione dell'input, rimuove i metadati preservando il profilo ICC e l'orientamento EXIF, e non è mai più grande dell'input. Aggiungi `targetSizeKb` per ridurre le dimensioni verso un budget; il target è "best effort", quindi leggi `result.fileSize` per vedere cosa è stato effettivamente ottenuto.

### Come converto qualsiasi documento in Markdown per una pipeline RAG?

Chiama `convertToMarkdown` con il file e passa `saveTo` per scrivere il `.md` direttamente su disco. Accetta 22 estensioni tra Office, OpenDocument, PDF, EPUB, HTML, CSV e testo semplice, e restituisce un unico file Markdown consapevole delle intestazioni, così il tuo chunker può dividere sulle intestazioni del documento invece che su conteggi arbitrari di caratteri.

### Come trasformo un intero sito web in chunk pronti per RAG con Node.js?

Chiama `client.v2.ingest({ mode: "sitemap", url, maxPages, chunk: { maxWords: 512, sentenceOverlap: 1 } })`. Ingest è sempre asincrono, quindi interroga `client.v2.getIngestJob(job.jobId)` finché `status` non è `"completed"` e leggi `outputUrl` per il JSONL firmato, oppure imposta `webhookUrl` e lascia che sia il webhook di completamento ad avvisarti. Per documenti locali invece che per un sito, `client.v2.ingestFiles([...])` esegue la stessa pipeline.

### Come estraggo JSON strutturato da una pagina con Node.js?

Chiama `client.v2.distill({ urls, schema })` dove `schema` è un oggetto JSON-Schema oppure una mappa piatta `{ campo: descrizione }`. Aggiungi un `cssSchema` e il passaggio sui selettori risponde a tutto ciò che può prima che qualcosa venga escalato al tier LLM; `result.extractionTier`, `fieldsFromCss` e `fieldsFromLlm` ti dicono quale tier ha fatto il lavoro.

### Come monitoro i cambiamenti di una pagina web con Node.js?

Chiama `client.v2.createWatcher(url, { frequencyMinutes: 60, diffMode: "auto", webhookUrl })`. Il limite minimo di un'ora è rigido, quindi 60 è la cadenza minima. Leggi lo storico con `getWatcherSnapshots`, metti in pausa con `updateWatcher(id, { status: "paused" })` ed elimina con `deleteWatcher`, che è una soft delete ed è idempotente.

### Come gestisce l'SDK le conversioni lunghe che superano il timeout del reverse-proxy?

Prima di ogni richiesta di conversione V1 l'SDK genera un UUID e lo invia come `job_id`; se la richiesta restituisce 5xx, interroga silenziosamente `GET /v1/convert/status/{job_id}` ogni 3 secondi finché il job non è `success` o `failed`. La scadenza del polling è di 5 minuti, dopodiché solleva `APIError(504, "Conversion timed out")`. V2 usa invece id di job espliciti, interrogati con `getPerceiveBatch` o `getIngestJob`.

### Posso usare l'SDK Node.js in un'applicazione browser?

No, l'SDK è solo lato server, perché si autentica con una chiave API privata (`sk_...`) che non deve mai finire nel bundle del codice lato client. Funziona su Node 18+, Bun e Deno tramite lo specificatore npm.

### Per quanto tempo è valido l'URL di download pre-firmato?

Il `presignedUrl` presente in ogni `ConversionResult` è valido per un'ora. Gli URL degli artefatti V2 sono validi 15 minuti e vengono rifirmati a ogni lettura, quindi `getPerceiveOperation(operationId)` ti consegna link freschi. Per un accesso permanente, scarica il file e archivialo nel tuo bucket.
