---
seo_title: Node.js SDK für Dateikonvertierung, npm | EnConvert
meta_desc: Installiere @enconvert/node-sdk per npm für Node.js 18+. Typisierte Konvertierungs-Methoden plus client.v2, um das Web wahrzunehmen, zu durchsuchen und zu überwachen.
keywords: sdk dateikonvertierung nodejs, npm client dateikonvertierung api, dateien konvertieren nodejs, url zu pdf nodejs sdk, heic zu webp nodejs, docx zu pdf node js, bild komprimieren nodejs, dokument zu markdown nodejs, alles zu pdf nodejs, typescript client konvertierungs api, enconvert node sdk, web scraping sdk nodejs, url zu markdown nodejs, strukturierte daten extrahieren nodejs, rag ingestion nodejs, webseite auf änderungen überwachen nodejs
---

# Node.js SDK für Dateikonvertierung

`@enconvert/node-sdk` ist der offizielle JavaScript- und TypeScript-Client für die EnConvert-API. Dreizehn typisierte Konvertierungsmethoden bilden REST-Endpunkte wie `POST /v1/convert/url-to-pdf` 1:1 ab, und ein zweiter Namensraum, `client.v2`, ergänzt Web-Intelligenz: eine URL in agentenfertige Artefakte überführen (perceive), die URLs einer Site ermitteln (discover), eine Websuche ausführen (lookup), strukturierte Daten destillieren (distill), eine Site als RAG-fertiges JSONL ingestieren (ingest) und Seiten auf Änderungen überwachen (watch). Das SDK zielt auf Node.js 18+ ohne Laufzeit-Abhängigkeiten, baut auf nativem `fetch`, `FormData` und `node:stream` auf und erholt sich transparent von Reverse-Proxy-Timeouts, indem es den Job-Status abfragt. Es wird mit dualen ESM- und CJS-Builds sowie vollständigen TypeScript-Deklarationen ausgeliefert.

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

---

## Installation

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

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

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

---

## Schnellstart

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

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

// V1: Eine URL in ein PDF konvertieren und auf die Festplatte streamen.
const result = await client.convertUrlToPdf("https://example.com", {
    saveTo: "page.pdf",
});
console.log(result.presignedUrl);

// V2: Eine Seite so lesen, wie dein Agent es tun sollte, mit angehängtem Qualitätswert.
const op = await client.v2.perceive("https://example.com", {
    outputs: ["markdown", "structured"],
});
console.log(op.outputs.markdown.url, op.renderQuality); // z. B. 0.93
```

Das SDK läuft in jeder modernen Node-Runtime (Node 18+, Bun, Deno über den npm-Spezifizierer). Es ist **ausschließlich serverseitig**, bündle deinen privaten API-Schlüssel also niemals in eine Browser-Anwendung.

---

## Was der Client bereitstellt

Ein Client, zwei Oberflächen. Beide sind über dieselbe `Enconvert`-Instanz erreichbar und teilen sich einen API-Schlüssel.

| Oberfläche | Erreichbar über | Was sie abdeckt |
|---------|-----------|----------------|
| Dateikonvertierung | `client.convertUrlToPdf(...)`, `client.convertImage(...)` und so weiter | Dreizehn typisierte Methoden für URL-Rendering, Bildkonvertierung, Bildkomprimierung, Dokumentkonvertierung sowie Job- und Website-Batch-Polling. Siehe [Dateikonvertierung](#dateikonvertierung). |
| Web-Intelligenz (V2) | `client.v2.perceive(...)`, `client.v2.distill(...)` und so weiter | Dreiundzwanzig Methoden über sechs Fähigkeiten hinweg: perceive, discover, lookup, distill, ingest, watch. Siehe [Web-Intelligenz (V2)](#web-intelligenz-v2). |

V2-Endpunkte verlangen einen privaten API-Schlüssel (`sk_...`); öffentliche Schlüssel werden abgelehnt. Wie sich die beiden Schlüsseltypen unterscheiden, steht unter [Authentifizierung](/de/docs/authentication.md), und die REST-Oberfläche hinter `client.v2` beschreibt die [V1 und V2](/de/docs/concepts/v1-and-v2.md).

---

## Dateikonvertierung

Die Konvertierungs-Oberfläche stellt dreizehn Methoden bereit, die die REST-API 1:1 abbilden:

| Methode | Endpunkt | Rückgabe |
|--------|----------|---------|
| `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}` (per Polling) | `BatchStatus` |

Jede Methode liefert ein typisiertes Promise. Alle Optionsfelder sind optional, sofern nicht anders vermerkt. Die letzten vier sind Batch-Helfer für ganze Websites: Sie reichen asynchrone Jobs ein und fragen sie ab, geben also eine `BatchSubmission` oder einen `BatchStatus` zurück statt eines `ConversionResult`.

---

### `convertUrlToPdf`

Rendert jede öffentliche URL als PDF.

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

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `saveTo` | `string` | -- | Lokaler Pfad, auf den das PDF gestreamt wird. Übergeordnete Verzeichnisse werden automatisch angelegt. |
| `singlePage` | `boolean` | `true` | `true` erzeugt eine durchgehende Seite. `false` paginiert anhand von `pdfOptions.pageSize`. |
| `pdfOptions` | `PdfOptions` | -- | Seitengröße, Ausrichtung, Ränder, Skalierung, Graustufen, Kopf- und Fußzeile. Siehe [PDF-Optionen](#pdf-optionen). |
| `viewportWidth` | `number` | `1920` | Breite des Browser-Viewports in Pixeln. |
| `viewportHeight` | `number` | `1080` | Höhe des Browser-Viewports in Pixeln. |
| `loadMedia` | `boolean` | `true` | Wartet vor der Aufnahme auf Bilder und Videos. |
| `enableScroll` | `boolean` | `true` | Scrollt von oben nach unten, um Lazy Loader auszulösen. |
| `outputFilename` | `string` | auto | Überschreibt den generierten Dateinamen. `.pdf` wird ergänzt, falls es fehlt. |

---

### `convertUrlToScreenshot`

Nimmt ein ganzseitiges PNG einer beliebigen URL auf.

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

Akzeptiert dieselben Optionen für Viewport, Medien, Scrollen und Dateinamen wie `convertUrlToPdf` (ohne `singlePage` und `pdfOptions`).

---

### `convertUrlToMarkdown`

Extrahiert sauberes GitHub-Flavored Markdown aus jeder URL. Der Konverter entfernt Navigation, Fußzeilen, Werbung und Skripte, behält den Hauptartikeltext und stellt YAML-Frontmatter voran (title, description, url, links, images).

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

Nützlich, um RAG-Pipelines zu bauen, fremde Inhalte in ein CMS zu importieren oder Trainingsdaten zu erzeugen. Wenn du zusätzlich zum Markdown einen Render-Qualitätswert möchtest, nimm stattdessen [`client.v2.perceive`](#perceive).

---

### `convertImage`

Konvertiert zwischen `jpeg`, `png`, `svg`, `heic` und `webp`.

```ts
// Aus einem Pfad
await client.convertImage("photo.heic", {
    outputFormat: "webp",
    saveTo: "photo.webp",
});

// Aus Bytes
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" },
);

// Ein SVG mit fester Breite rastern
await client.convertImage("logo.svg", { outputFormat: "png", width: 512, saveTo: "logo.png" });
```

Das Eingabeformat wird aus der Endung des Pfads bzw. Dateinamens erkannt. Das Ausgabeformat ist erforderlich.

| Option | Typ | Erforderlich | Beschreibung |
|--------|------|----------|-------------|
| `outputFormat` | `string` | Ja | Zielformat: `jpeg`, `png`, `svg`, `heic` oder `webp` (und `jpeg` bei einer `.pdf`-Eingabe). Die Aliase `jpg`, `yml`, `htm` und `md` werden normalisiert. Nicht unterstützte Paare werfen einen Fehler, bevor die Anfrage rausgeht. |
| `saveTo` | `string` | -- | Lokaler Pfad, auf den das Ergebnis gestreamt wird. |
| `outputFilename` | `string` | -- | Überschreibt den generierten Dateinamen. |
| `width` | `number` | -- | Nur bei SVG-Eingabe (`svg-to-png`, `svg-to-jpeg`, `svg-to-webp`), 1 bis 10000. Allein gesetzt skaliert der Wert proportional und übernimmt die Höhe aus dem Seitenverhältnis des SVG. |
| `height` | `number` | -- | Nur bei SVG-Eingabe (`svg-to-png`, `svg-to-jpeg`, `svg-to-webp`), 1 bis 10000. Allein gesetzt skaliert der Wert proportional und übernimmt die Breite aus dem Seitenverhältnis des SVG. |

Setze `width` und `height` gemeinsam, um eine exakte Leinwand festzulegen, was das Seitenverhältnis verändern kann. Lässt du beide weg, behält die Ausgabe die intrinsische Breite, Höhe oder `viewBox` des SVG. Die Gesamtzahl der Ausgabepixel ist auf 25.000.000 begrenzt. Keine der beiden Optionen wird von `svg-to-heic` akzeptiert, und das SDK wirft einen Fehler, bevor es die Anfrage sendet, wenn du sie einer anderen Konvertierung übergibst.

---

### `convertDocument`

Konvertiert Dokumente und Datenformate. Der Standardwert von `outputFormat` ist `"pdf"`.

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

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

// markdown zu pdf mit eigenem Seitenlayout
await client.convertDocument("README.md", {
    outputFormat: "pdf",
    pdfOptions: { pageSize: "A4", margins: { top: 20, bottom: 20, left: 25, right: 25 } },
    saveTo: "readme.pdf",
});
```

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

Für EPUB gibt es kein eigenes Dokumentkonvertierungs-Paar. Schicke `.epub`-Dateien stattdessen durch [`convertToPdf`](#converttopdf) oder [`convertToMarkdown`](#converttomarkdown).

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `outputFormat` | `string` | `"pdf"` | Zielformat. |
| `saveTo` | `string` | -- | Lokaler Pfad, auf den das Ergebnis gestreamt wird. |
| `outputFilename` | `string` | -- | Überschreibt den generierten Dateinamen. |
| `pdfOptions` | `PdfOptions` | -- | Seitenlayout. Wird nur berücksichtigt, wenn die Ausgabe ein PDF ist. |

---

### `compressImage`

Verkleinert ein PNG, JPEG oder WebP, ohne das Format zu ändern.

```ts
// Nur der verlustfreie Durchgang
const result = await client.compressImage("photo.jpg", { saveTo: "photo-small.jpg" });

// Auf ein Budget von 200 KB zielen
const capped = await client.compressImage("photo.jpg", {
    targetSizeKb: 200,
    saveTo: "photo-capped.jpg",
});

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

**Unterstützte Eingaben:** `.png`, `.jpg`, `.jpeg`, `.webp`.

Die Ausgabe behält Format und Endung der Eingabe, es gibt hier also kein Ausgabeformat zu wählen. Die erste Stufe ist verlustfrei: Metadaten werden entfernt, ICC-Profil und EXIF-Ausrichtung bleiben erhalten, und das Ergebnis ist nie größer als die Eingabe. `targetSizeKb` ergänzt eine zweite Stufe, die bei gesperrtem Seitenverhältnis herunterskaliert, bis das Budget erreicht ist. Dieses Ziel gilt als Best Effort: Ein unerreichbares Budget liefert die kleinste erzielte Datei statt eines Fehlers, prüfe also `result.fileSize`. Animierte APNG- und animierte WebP-Dateien werden mit `400` abgelehnt, und die dekodierte Leinwand ist auf 40.000.000 Pixel begrenzt.

| Option | Typ | Erforderlich | Beschreibung |
|--------|------|----------|-------------|
| `targetSizeKb` | `number` | -- | Größenbudget in KB, ganzzahlig, Minimum `1`. Weglassen, um nur den verlustfreien Durchgang auszuführen. |
| `saveTo` | `string` | -- | Lokaler Pfad, auf den das Ergebnis gestreamt wird. |
| `outputFilename` | `string` | -- | Überschreibt den generierten Dateinamen. Die Eingabe-Endung bleibt erhalten. |

---

### `convertToMarkdown`

Konvertiert jede unterstützte Dokument-, Tabellen-, Präsentations-, E-Book-, Web- oder Klartextdatei nach Markdown.

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

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

Die Ausgabe ist eine einzelne, überschriftenbewusste `.md`-Datei, gebaut für RAG-Chunking: Die Überschriftenhierarchie des Dokuments übersteht die Konvertierung, sodass ein semantischer Chunker an Überschriften trennen kann statt an willkürlichen Zeichenzahlen. Dieser Endpunkt kennt keine PDF-Optionen. Jede andere Endung wirft einen Fehler, bevor eine Anfrage gestellt wird.

| Option | Typ | Erforderlich | Beschreibung |
|--------|------|----------|-------------|
| `saveTo` | `string` | -- | Lokaler Pfad, auf den das Markdown gestreamt wird. |
| `outputFilename` | `string` | -- | Überschreibt den generierten Dateinamen. |

Wenn das Chunking gleich mit erledigt werden soll, übergib dieselben Dateien an [`client.v2.ingestFiles`](#ingest).

---

### `convertToPdf`

Konvertiert jede unterstützte Dokument-, Bild-, E-Book-, Web- oder Klartextdatei nach PDF.

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

// html zu pdf mit vollständiger Seitengeometrie
await client.convertToPdf("invoice.html", {
    pdfOptions: { pageSize: "A4", margins: { top: 15, bottom: 15, left: 15, right: 15 } },
    saveTo: "invoice.pdf",
});

// pdf zu Graustufen-pdf (Durchreichen)
await client.convertToPdf("scan.pdf", {
    pdfOptions: { grayscale: true },
    saveTo: "scan-gray.pdf",
});
```

**Unterstützte Eingaben (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`.

Eine `.pdf`-Eingabe wird akzeptiert und durchgereicht, mit `pdfOptions: { grayscale: true }` dient diese Methode also zugleich als PDF-Normalisierungspfad. EPUB wird ebenfalls hier abgewickelt, da es kein eigenes Dokumentkonvertierungs-Paar hat. Jede andere Endung wirft einen Fehler, bevor eine Anfrage gestellt wird.

<div class="alert alert-warning">
<strong>Die Geometrie hängt von der Eingabe ab.</strong> Die vollständige Seitengeometrie (Seitengröße, Seitenbreite und -höhe, Ausrichtung, Ränder, Skalierung, Kopfzeile, Fußzeile) wird bei HTML (<code>.html</code>, <code>.htm</code>, <code>.xhtml</code>), Markdown, Klartext, EPUB, Bild- und SVG-Eingaben berücksichtigt. Office-, ODF-, iWork-, RTF- und CSV-Eingaben sowie das Durchreichen von PDFs unterstützen nur <code>grayscale</code> und antworten mit <code>400</code>, wenn eine explizite Geometrie-Option gesetzt ist. <code>grayscale</code> selbst wird bei jeder Eingabe berücksichtigt.
</div>

| Option | Typ | Erforderlich | Beschreibung |
|--------|------|----------|-------------|
| `saveTo` | `string` | -- | Lokaler Pfad, auf den das PDF gestreamt wird. |
| `outputFilename` | `string` | -- | Überschreibt den generierten Dateinamen. `.pdf` wird ergänzt, falls es fehlt. |
| `pdfOptions` | `PdfOptions` | -- | Seitenlayout. Siehe den Hinweis oben dazu, welche Eingaben die Geometrie berücksichtigen. |

---

### `getJobStatus`

Fragt den Status eines asynchronen oder wiederhergestellten Jobs ab.

```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);
}
```

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

<div class="alert alert-info">
<strong>In der Regel musst du das nicht direkt aufrufen.</strong> Das SDK fragt automatisch ab, wenn eine synchrone Anfrage 5xx liefert. Siehe <a href="#timeout-recovery">Timeout-Recovery</a> weiter unten.
</div>

---

### Batch-Helfer für ganze Websites

`convertWebsiteToPdf` und `convertWebsiteToScreenshot` ermitteln die Seiten einer Website, stellen sie alle in die Warteschlange und bündeln die Ausgaben in einem ZIP. Beide liefern sofort eine `BatchSubmission`; frage anschließend mit `getBatchStatus` ab oder blockiere mit `waitForBatch`. Gemeinsame Optionen sind `crawlMode` (`"auto"`, `"sitemap"` oder `"full"`), `includePatterns`, `excludePatterns`, `notificationEmail` und `callbackUrl`; `convertWebsiteToPdf` ergänzt `singlePage` und `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` akzeptiert `intervalMs` (Standard `5_000`), `timeoutMs` (Standard `1_800_000`, also dreißig Minuten) und `saveTo`. Wird die Frist überschritten, wirft es `APIError(504, ...)`. Die REST-Oberfläche beschreibt die [Endpunkt-Übersicht](/de/docs/endpoints.md).

---

## Web-Intelligenz (V2)

Alles unterhalb von `client.v2` liefert Daten, denen ein Agent vertrauen kann, denn jedes V2-Rendering trägt einen `renderQuality`-Wert zwischen 0.0 und 1.0. Eine blockierte Seite, eine Bot-Challenge, eine Login-Schranke, eine HTTP-Fehlerseite, ein Soft-404 oder eine leere SPA-Hülle kommt mit niedrigem Wert samt benannten `deductions` und `warnings` zurück und wird damit markiert, statt für echten Inhalt gehalten zu werden. Der Inhalt wird trotzdem zurückgegeben; was du damit machst, entscheidest du. Werte unterhalb von etwa 0.40 bedeuten, dass das Rendering in keinem sinnvollen Sinn geglückt ist.

Dreiundzwanzig Methoden über sechs Fähigkeiten hinweg:

| Methode | Endpunkt | Rückgabe |
|--------|----------|---------|
| `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` |

Optionen sind auf der SDK-Oberfläche in camelCase und werden in das snake_case-Wire-Format der API serialisiert; Antworten werden zurück nach camelCase abgebildet. Deine eigenen Payloads (Extraktionsschemata, extrahierte Daten, verfolgte Felder, Diff-Einträge) werden unverändert durchgereicht.

---

### Perceive

Rendert eine URL in die Artefakte, die du anforderst: Markdown, bereinigtes oder rohes HTML, einen Viewport- oder Ganzseiten-Screenshot, ein PDF, eine Linkliste, eine Bildliste oder strukturierte Daten. `perceive` arbeitet synchron und liefert die abgeschlossene Operation mit 15 Minuten gültigen signierten Artefakt-URLs. Vollständige Referenz: [Perceive](/de/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);        // 0.0 bis 1.0
console.log(op.deductions);           // z. B. { http_error: 0.7 }
console.log(op.outputs.markdown.url); // 15 Minuten gültige signierte URL
console.log(op.structured);

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

// Artefakt-URLs später neu signieren, ohne erneut zu rendern:
const again = await client.v2.getPerceiveOperation(op.operationId);
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `outputs` | `PerceiveOutputName[]` | `["markdown", "structured"]` | Beliebige aus `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `extract` | `PerceiveExtractName[]` | -- | Heuristische Ziele: `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `onlyMainContent` | `boolean` | `true` | Entfernt Navigation, Header, Footer und Cookie-Banner aus der Markdown-Ausgabe, abgesichert durch einen Fidelity-Guard. `false` liefert die vollständige Seite unangetastet. |
| `schema` | `Record<string, unknown>` | -- | JSON-Schema für die strukturierte Extraktion auf der LLM-Stufe. |
| `waitFor` | `string` | -- | CSS-Selektor (optional als `"css:..."`) oder `"js:<expr>"`, auf den vor der Aufnahme gewartet wird. |
| `waitTimeoutMs` | `number` | `30000` | 0 bis 60000. |
| `jsCode` | `string` | -- | JavaScript, das nach der Navigation ausgeführt wird. Maximal 20000 Zeichen. |
| `viewport` | `{ width?, height? }` | `1920 x 1080` | Breite 320 bis 3840, Höhe 240 bis 2160. |
| `headers` | `Record<string, string>` | -- | Zusätzliche Request-Header. |
| `cookies` | `BrowserCookie[]` | -- | Cookies, die vor dem Rendern gesetzt werden. Jedes braucht `name`, `value` und entweder `domain` oder `url`. |
| `auth` | `{ username, password }` | -- | HTTP Basic Auth. |
| `cacheMode` | `"enabled" \| "bypass" \| "refresh"` | `"enabled"` | Cache für eine Stunde. `bypass` überspringt ihn, `refresh` erzwingt ein neues Rendering. |
| `pdfOptions` | `PdfOptions` | -- | Nur sinnvoll, wenn `outputs` den Wert `"pdf"` enthält. Siehe [PDF-Optionen](#pdf-optionen). |
| `blockResources` | `PerceiveResourceType[]` | -- | Ressourcentypen, die der Browser nicht laden soll, zum Beispiel `["image", "font", "media"]`. |
| `respectRobots` | `boolean` | -- | Beachtet die Robots-Regeln der Website. |
| `mobile` | `boolean` | -- | Rendert mit einem mobilen Profil. |
| `directDownload` | `boolean` | -- | Nur bei `perceive`. Nimm besser `perceiveDirect`, das die Option für dich setzt. |

<div class="alert alert-warning">
<strong>Drei Optionen sind deklariert, aber noch nicht aktiv.</strong> <code>proxyUrl</code>, <code>geolocation</code> und <code>actionChain</code> sind auf <code>PerceiveOptions</code> typisiert, werden serverseitig aber derzeit mit <code>422</code> abgelehnt. Sie sind reserviert, nicht nutzbar.
</div>

**Direkter Download.** `perceiveDirect` spart den Umweg über die signierte URL: Der HTTP-Antwortkörper enthält die Artefakt-Bytes, und die Metadaten reisen in den Headern mit. Es verlangt genau eine artefakterzeugende Ausgabe, und das SDK wirft lokal einen Fehler vor dem Senden, wenn das nicht der Fall ist (`"structured"` darf mitfahren, bleibt aber serverseitig inline und wird nicht zurückgegeben).

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

// Ein gespeichertes Artefakt einer früheren Operation erneut als rohe Bytes laden.
// `output` darf entfallen, wenn die Operation genau ein Artefakt erzeugt hat.
const raw = await client.v2.downloadPerceiveArtifact(op.operationId, "markdown");
```

`downloadPerceiveArtifact` liefert `410`, sobald das gespeicherte Artefakt abgelaufen ist, und `400` (mit Auflistung der verfügbaren Ausgaben), wenn die Operation mehr als ein Artefakt erzeugt hat und du `output` weggelassen hast.

**Batches.** `perceiveBatch` nimmt bis zu 1000 URLs mit einem gemeinsamen Optionsblock entgegen. Kleine Batches laufen inline und kommen fertig zurück; größere liefern den Status `"queued"`, frage sie also mit der zurückgegebenen `jobId` über `getPerceiveBatch` ab.

```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` ist `"manifest"` (Standard, ein Eintrag pro URL in `items`) oder `"zip"` (jedes erfolgreiche Artefakt wird gebündelt, sobald der Job fertig ist). Der Batch-Endpunkt lehnt `directDownload` ab; nutze stattdessen `outputMode: "zip"`.

---

### Discover

Listet die URLs einer Website auf, ohne etwas zu rendern. Es ist kein Browser beteiligt, das macht es schnell und günstig im Vergleich dazu, jede Seite wahrzunehmen. Vollständige Referenz: [Discover](/de/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); // z. B. { sitemap: 42, crawl: 30 }
for (const url of found.urls) console.log(url);
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `mode` | `"sitemap" \| "crawl" \| "hybrid"` | `"hybrid"` | Nur Sitemap, nur HTTP-Crawl oder beides. |
| `maxUrls` | `number` | `100` | 1 bis 1000. `truncated` ist `true`, wenn es mehr URLs gab, als dieses Limit zuließ. |
| `maxDepth` | `number` | `2` | 1 bis 5. Crawl-Tiefe ab der Start-URL. |
| `includePatterns` | `string[]` | -- | Regex-Positivliste, maximal 50 Einträge. |
| `excludePatterns` | `string[]` | -- | Regex-Negativliste, angewandt nach `includePatterns`, maximal 50 Einträge. |
| `sameDomainOnly` | `boolean` | `true` | Hält den Crawl auf der Start-Domain. |
| `respectRobots` | `boolean` | -- | Beachtet die Robots-Regeln der Website. `robotsRespected` im Ergebnis meldet, was tatsächlich passiert ist. |

---

### Lookup

Führt eine kategorisierte Websuche aus und nimmt auf Wunsch die besten Treffer automatisch wahr, sodass jeder Treffer sein eigenes vollständiges `PerceiveResult` inline mitbringt. Vollständige Referenz: [Lookup](/de/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);
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `category` | `"web" \| "news" \| "images" \| "scholar" \| "patents" \| "maps"` | `"web"` | Suchvertikale. |
| `country` | `string` | -- | Google-`gl`-Ländercode, zum Beispiel `"us"` oder `"in"`. |
| `locale` | `string` | -- | Google-`hl`-Oberflächensprache, zum Beispiel `"en"`. |
| `timeFilter` | `"hour" \| "day" \| "week" \| "month" \| "year"` | -- | Aktualitätsfenster. |
| `numResults` | `number` | `10` | 1 bis 100. |
| `page` | `number` | `1` | 1 bis 10. |
| `location` | `string` | -- | Freitext-Ort, zum Beispiel `"Austin, Texas"`. |
| `autocorrect` | `boolean` | `true` | Lässt den Anbieter offensichtliche Tippfehler korrigieren. |
| `perceiveTop` | `number` | `0` | 0 bis 10. Rendert die Top-N-Ergebnis-URLs automatisch; jede davon ist ein vollständiges Browser-Rendering. |

`perceiveTop` im Ergebnis meldet, wie viele Treffer tatsächlich wahrgenommen wurden, was weniger sein kann als angefordert, und `perceiveOperationIds` liefert dir die Operations-IDs zum späteren Neusignieren.

---

### Distill

Schemagesteuerte strukturierte Extraktion. Gib eine Form und eine Menge URLs vor (oder eine Website, die zuerst ermittelt werden soll), und du bekommst Datensätze in dieser Form zurück. Ein optionales `cssSchema` beantwortet alles, was es über Selektoren kann, bevor irgendetwas auf die LLM-Stufe eskaliert. Vollständige Referenz: [Distill](/de/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);
```

Übergib genau eines von `urls` oder `discoverFrom`; das SDK wirft lokal einen Fehler, wenn du beides oder keines übergibst, und ebenso, wenn `schema` fehlt oder kein Objekt ist.

```ts
// Zuerst eine Website ermitteln, dann jede gefundene Seite destillieren.
await client.v2.distill({
    discoverFrom: { url: "https://example.com", mode: "sitemap", maxPages: 10 },
    schema: { title: "page title", summary: "one-line summary" },
});
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `urls` | `string[]` | -- | Explizite URLs zum Destillieren, maximal 50. Schließt `discoverFrom` aus. |
| `discoverFrom` | `{ url, mode?, maxPages? }` | -- | Erst ermitteln, dann destillieren. `maxPages` liegt zwischen 1 und 50, Standard 10, und begrenzt sowohl Ermittlung als auch Destillation. |
| `schema` | `Record<string, unknown>` | erforderlich | Ein JSON-Schema-Objekt (`{ type: "object", properties: {...} }`) oder eine flache `{ field: description }`-Zuordnung. |
| `cssSchema` | `CssSchema` | -- | Kostenloser Selektor-Durchgang vor jeder LLM-Eskalation. |
| `waitFor` | `string` | -- | CSS-Selektor oder `"js:<expr>"`, auf den gewartet wird. |
| `waitTimeoutMs` | `number` | `30000` | 0 bis 60000. |
| `headers` | `Record<string, string>` | -- | Zusätzliche Request-Header. |
| `cookies` | `BrowserCookie[]` | -- | Cookies, die vor dem Rendern gesetzt werden. |
| `respectRobots` | `boolean` | -- | Beachtet die Robots-Regeln der Website. |

Ein `CssSchema` besitzt einen `baseSelector` (den sich wiederholenden Container, ein Datensatz pro Treffer), eine `fields`-Liste, einen optionalen `name` und ein optionales `targetField`, das die Eigenschaft des Ausgabeschemas benennt, welche die Datensätze füllen. Jedes Feld hat die Form `{ name, type, selector?, attribute?, pattern?, default?, transform?, fields? }`, wobei `type` einer der Werte `text`, `attribute`, `html`, `regex`, `nested`, `list`, `nested_list` ist. `attribute` ist bei `attribute`-Feldern erforderlich, `pattern` bei `regex`-Feldern und ein nicht leeres `fields`-Array bei den verschachtelten Typen (maximale Tiefe 5).

---

### Ingest

Verwandelt eine Website oder einen Stapel hochgeladener Dokumente in gechunktes, RAG-fertiges JSONL. Ingest läuft immer asynchron: Beide Einstiegspunkte liefern einen eingereihten `IngestJob`, und du fragst ihn entweder ab oder richtest einen Webhook ein. Vollständige Referenz: [Ingest](/de/docs/endpoints/ingest.md).

```ts
// Von einer Website.
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",
});

// Von hochgeladenen Dateien: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD
// sowie alte oder ODF-Office-Dokumente.
const fileJob = await client.v2.ingestFiles(["handbook.pdf", "notes.docx"], {
    chunk: { maxWords: 512, sentenceOverlap: 1 },
});

// Beide werden gleich abgefragt. Nicht-terminale Zustände: 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); // idempotent
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `mode` | `"urls" \| "sitemap" \| "crawl"` | `"urls"` | `"urls"` braucht `urls` und lehnt `url` ab. `"sitemap"` und `"crawl"` brauchen eine Start-`url` und lehnen `urls` ab. Beide Regeln werden lokal vor der Anfrage geprüft. Die `IngestMode`-Union kennt außerdem `"files"`, was `ingestFiles` bei seinem Job meldet; übergib es hier nicht. |
| `url` | `string` | -- | Start-URL für `sitemap` und `crawl`. |
| `urls` | `string[]` | -- | Explizite URLs für den Modus `"urls"`, maximal 1000. |
| `maxPages` | `number` | `50` | Ermittlungs-Limit für `sitemap` und `crawl`, 1 bis 1000. |
| `maxDepth` | `number` | `2` | 1 bis 5. |
| `sameDomainOnly` | `boolean` | `true` | Hält den Crawl auf der Start-Domain. |
| `includePatterns` / `excludePatterns` | `string[]` | -- | Regex-Positiv- und Negativliste. |
| `respectRobots` | `boolean` | -- | Beachtet die Robots-Regeln der Website. |
| `waitFor` / `waitTimeoutMs` | `string` / `number` | -- / `30000` | Wartezeit pro Seite beim Rendern. |
| `chunk` | `{ maxWords?, sentenceOverlap? }` | `512` / `1` | `maxWords` liegt zwischen 32 und 4000, `sentenceOverlap` zwischen 0 und 10. |
| `webhookUrl` | `string` | -- | Webhook bei Fertigstellung, HMAC-signiert. |

`ingestFiles` nimmt ein `FileInput[]` entgegen, also Pfad-Strings, `Uint8Array` / `Buffer` oder `{ data, filename, contentType? }`-Objekte in beliebiger Mischung. Es akzeptiert nur `chunk` und `webhookUrl` und wirft bei einer leeren Liste lokal einen Fehler.

**Webhook-Signierung.** Webhooks bei Fertigstellung sind HMAC-signiert. Hole das Secret (es wird beim ersten Aufruf erzeugt), um Zustellungen zu prüfen, rotiere es bei Bedarf und stelle einen Webhook erneut zu, den dein Endpunkt verpasst hat.

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

// Rotieren macht Signaturen des vorherigen Secrets sofort ungültig.
const rotated = await client.v2.rotateWebhookSecret();

// Den Webhook eines abgeschlossenen Jobs erneut zustellen.
const retry = await client.v2.retryIngestWebhook(job.jobId);
console.log(retry.delivered, retry.attempts, retry.statusCode, retry.detail);
```

`retryIngestWebhook` liefert `409`, wenn der Job nicht abgeschlossen ist, und `400`, wenn für den Job kein Webhook konfiguriert wurde.

---

### Watch

Erstellt einen Watcher, der eine URL in festem Takt neu rendert und dich benachrichtigt, wenn sich die Seite ändert: per E-Mail, per Webhook oder beides. Vollständige Referenz: [Watch](/de/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: "" }); // löscht den Webhook
await client.v2.deleteWatcher(watcher.watcherId);                     // Soft Delete, idempotent
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `frequencyMinutes` | `number` | `60` | Minuten zwischen zwei Prüfungen, 60 bis 43200. Die Stundengrenze ist hart. |
| `diffMode` | `"auto" \| "text" \| "structured" \| "tables" \| "metadata"` | `"auto"` | `"auto"` lässt die Diff-Engine nach Inhaltstyp entscheiden. |
| `trackFields` | `Record<string, unknown>` | -- | Feld- oder Selektor-Auswahl, um den Diff einzugrenzen. |
| `webhookUrl` | `string` | -- | Webhook für Änderungsbenachrichtigungen, HMAC-signiert. |
| `notifyEmail` | `boolean` | `true` | Benachrichtigt den Projektinhaber bei Änderungen per E-Mail. |

`updateWatcher` nimmt dieselben Felder plus `status` (`"active"` oder `"paused"`) und verlangt mindestens eines davon; bei einem leeren Update wirft das SDK lokal einen Fehler. `webhookUrl: ""` löscht den Webhook explizit. Das Löschen ist ein Soft Delete: `deleteWatcher` liefert den stillgelegten Watcher mit Status `"deleted"` zurück, und ein gelöschter Watcher antwortet bei `getWatcher` mit `404`.

Jeder Snapshot trägt `checkedAt`, `hasChanges`, `similarity` (0.0 bis 1.0 gegenüber der vorherigen Aufnahme), `renderQuality`, `changeCount` und ein `changes`-Array.

<div class="alert alert-warning">
<strong>Snapshot-Diffs enthalten nicht vertrauenswürdige Seiteninhalte.</strong> Einträge in <code>snapshot.changes</code> stammen direkt von der überwachten Seite. Escape sie, bevor du sie in HTML renderst oder in einen Log-Viewer schreibst.
</div>

---

## PDF-Optionen

Werden über das Feld `pdfOptions` an `convertUrlToPdf`, `convertDocument`, `convertToPdf`, `convertWebsiteToPdf` und `client.v2.perceive` übergeben (bei letzterem, wenn `outputs` den Wert `"pdf"` enthält).

```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",
});
```

| Feld | Typ | Beschreibung |
|-------|------|-------------|
| `pageSize` | `string` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` usw. |
| `pageWidth` / `pageHeight` | `number` | Explizite Seitengeometrie als Alternative zu `pageSize`. |
| `orientation` | `"portrait" \| "landscape"` | Standard ist Hochformat. |
| `margins` | `{ top, bottom, left, right }` (mm) | Alle vier sind optional. |
| `scale` | `number` | Render-Skalierung, z. B. `0.9` für 90 %. |
| `grayscale` | `boolean` | Wandelt das PDF per Ghostscript-Nachbearbeitung in Graustufen um. |
| `header` | `PdfHeaderFooter` | `{ content?, height? }`. `content` ist auf 2000 Zeichen begrenzt. |
| `footer` | `PdfHeaderFooter` | Gleiche Form wie `header`. |

Jeder Parameter ist vollständig unter [Synchrone und asynchrone Jobs](/de/docs/concepts/sync-and-async.md) beschrieben.

## Fehlerbehandlung

Fehler sind typisierte Exception-Klassen, die du per `instanceof` abgleichen kannst. Dieselbe Hierarchie deckt sowohl die Konvertierungsmethoden als auch `client.v2` ab.

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

| Klasse | Ausgelöst bei | Statuscode |
|-------|-----------|-------------|
| `AuthenticationError` | Ungültiger, fehlender oder widerrufener Schlüssel | `401`, `403` (beide melden `statusCode` `401`) |
| `QuotaError` | Wird bei HTTP 402 ausgelöst | `402` |
| `RateLimitError` | Zu viele Anfragen | `429` |
| `APIError` | Jeder andere 4xx-/5xx-Fehler | der tatsächliche Code |
| `EnconvertError` | Basisklasse für alle oben genannten | -- |

`QuotaError` und `RateLimitError` erweitern beide `APIError`, das wiederum `EnconvertError` erweitert. Ordne deine `instanceof`-Prüfungen also von spezifisch nach allgemein. Jeder `APIError` trägt ein Feld `statusCode`.

Manche Fehler erreichen das Netzwerk nie: eine nicht unterstützte Dateiendung, ein `distill`-Aufruf mit `urls` und `discoverFrom` gleichzeitig, ein `ingest`-Aufruf, dessen Modus und Argumente nicht zusammenpassen, ein `perceiveDirect`-Aufruf mit mehr als einer Artefakt-Ausgabe oder ein `updateWatcher`-Aufruf ohne Felder. Diese werfen lokal einen einfachen `Error`, damit du den Fehler in der Entwicklung findest.

Die vollständige Zuordnung der Fehlermeldungen steht in der Referenz [Fehlercodes](/de/docs/reference/errors.md).

---

## Timeout-Recovery

Lange URL-zu-PDF-Vorgänge oder große Dokumentkonvertierungen können das Reverse-Proxy-Timeout von 60 bis 120 Sekunden überschreiten, selbst wenn die Konvertierung auf dem Server am Ende gelingt. Das SDK erledigt das bei den V1-Konvertierungsmethoden transparent:

1. Vor jeder Anfrage erzeugt das SDK eine UUID und sendet sie als `job_id` im Request-Body.
2. Liefert die ursprüngliche Anfrage 5xx, wechselt das SDK still auf Polling von `GET /v1/convert/status/{job_id}` alle 3 Sekunden.
3. Sobald der Job als `success` verbucht ist, gibt das SDK das Ergebnis zurück. Sobald er als `failed` verbucht ist, wirft das SDK `APIError`.
4. Die Polling-Frist beträgt 5 Minuten. Wird sie überschritten, wirft das SDK `APIError(504, "Conversion timed out")`.

Dafür musst du keine Zeile Code schreiben, es funktioniert einfach. Setze `timeout` im Konstruktor, wenn du die erste Anfrage begrenzen willst.

V2 nutzt statt impliziter Recovery explizite Job-Objekte: `perceiveBatch` und `ingest` liefern eine ID, die du mit `getPerceiveBatch` bzw. `getIngestJob` abfragst, und `ingest` kann stattdessen einen Webhook aufrufen.

---

## Konfiguration

```ts
const client = new Enconvert({
    apiKey: process.env.ENCONVERT_API_KEY!,
    timeout: 300_000, // ms, Standard 5 Min.
    baseUrl: "https://api.enconvert.com", // für selbst gehostete Gateways überschreiben
});
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `apiKey` | `string` | -- (erforderlich) | Privater API-Schlüssel (`sk_...`). Der Konstruktor wirft sofort einen Fehler, wenn er fehlt. |
| `timeout` | `number` | `300_000` | Request-Timeout in ms. Bricht das zugrunde liegende `fetch` über `AbortController` ab. |
| `baseUrl` | `string` | `https://api.enconvert.com` | Basis-URL der API. Schrägstriche am Ende werden entfernt. |

Der Schlüssel reist bei jeder Anfrage als `X-API-Key`-Header mit, bei V1 wie bei V2. `client.v2` wird für dich konstruiert und teilt sich Schlüssel, Basis-URL und Timeout des Clients, es gibt also nichts zusätzlich zu konfigurieren.

<div class="alert alert-warning">
<strong>Hardcode den API-Schlüssel niemals.</strong> Lies ihn aus einer Umgebungsvariable oder deinem Secret-Manager. Wer deinen privaten Schlüssel erhält, kann Konvertierungen und V2-Operationen auf deinem Konto ausführen. Schlüssel rotierst du im <a href="/de/dashboard">Dashboard</a>.
</div>

---

## Form des Ergebnisses

Jede Konvertierungsmethode liefert ein `ConversionResult`:

```ts
interface ConversionResult {
    presignedUrl: string;          // signierte URL zum Download der Ausgabe (1 Stunde)
    objectKey: string;             // Objektschlüssel im Speicher
    filename: string;              // serverseitiger Dateiname
    fileSize?: number;             // Bytes
    conversionTimeSeconds?: number;
    jobId?: string;                // vorhanden, wenn die Timeout-Recovery abgefragt hat
}
```

Die vorsignierte URL ist eine Stunde lang gültig. Brauchst du dauerhaften Zugriff, lade die Datei herunter (per `saveTo` oder indem du die URL selbst abrufst) und lege sie in deinem eigenen Bucket ab.

V2-Ergebnisse sind anders aufgebaut. Ein `PerceiveResult` trägt `operationId`, `status`, `url`, `urlFinal`, `contentHash`, `renderQuality`, `statusCode`, `deductions`, `cacheHit`, eine nach Ausgabenamen geschlüsselte `outputs`-Zuordnung, `structured`, `extractionTier`, `tokens`, `costCents`, `durationMs`, `optionsEcho`, `error` und `warnings`. Jeder Eintrag in `outputs` ist ein `V2OutputArtifact` der Form `{ url?, objectKey, sizeBytes, contentType, expiresIn }`, wobei `expiresIn` in Sekunden angegeben ist und standardmäßig `900` beträgt. V2-Artefakt-URLs halten also 15 Minuten statt einer Stunde und werden bei jedem Lesen neu signiert. Ein erneuter Aufruf von `getPerceiveOperation(operationId)` liefert dir daher frische Links, ohne die Seite neu zu rendern.

---

## TypeScript

Die Typdefinitionen werden mit dem Paket ausgeliefert, eine `@types/...`-Installation ist also nicht nötig. Das Paket wird dual veröffentlicht (ESM + CJS) mit sauberen `exports`, `types` sowie `.d.ts` / `.d.cts`, sodass es unter jedem Node-Modulauflösungsmodus funktioniert.

```ts
import type {
    ClientOptions,
    CompressImageOptions,
    ConversionResult,
    ConvertDocumentOptions,
    ConvertImageOptions,
    ConvertToMarkdownOptions,
    ConvertToPdfOptions,
    FileInput,
    JobStatus,
    PdfOptions,
    UrlToMarkdownOptions,
    UrlToPdfOptions,
    UrlToScreenshotOptions,
    // Die V2-Typen kommen aus demselben Einstiegspunkt.
    DiscoverOptions, DiscoverResult,
    DistillOptions, DistillResult,
    IngestJob, IngestOptions,
    LookupOptions, LookupResult,
    PerceiveOptions, PerceiveResult, PerceiveOutputName,
    PerceiveBatchResult, PerceiveDirectResult,
    Watcher, WatcherSnapshotList,
} from "@enconvert/node-sdk";
```

Die Klasse `EnconvertV2` selbst wird ebenfalls exportiert, falls du einen Funktionsparameter als V2-Namensraum typisieren willst.

---

## Aktualisieren

Das Paket bringt eine kleine CLI mit, `enconvert-sdk`, um sich selbst aktuell zu halten.

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

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

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

`upgrade` erkennt npm, pnpm, yarn oder bun aus der Umgebung und gibt den exakten Installationsbefehl immer aus, bevor es ihn ausführt. So passiert mit deiner Lockfile nichts unbemerkt. `--dry-run` gibt diesen Befehl aus und hört dann auf. `version` meldet die installierte SDK-Version.

---

## Quellcode und Issues

- **npm:** [@enconvert/node-sdk](https://www.npmjs.com/package/@enconvert/node-sdk)
- **GitHub:** [enconvert/node-sdk](https://github.com/enconvert/node-sdk)
- **Lizenz:** MIT
- **Andere Sprachen:** [Alle SDKs](/de/docs/guides/integrations/sdks.md)

---

## Häufig gestellte Fragen

### Wie konvertiere ich Dateien in Node.js mit einem npm-Paket?

Installiere `@enconvert/node-sdk`, erzeuge einen Client mit deinem API-Schlüssel (`new Enconvert({ apiKey: process.env.ENCONVERT_API_KEY! })`) und rufe eine typisierte Methode wie `convertUrlToPdf`, `convertImage` oder `convertDocument` auf. Übergib `saveTo`, um das Ergebnis direkt auf die Festplatte zu streamen.

### Wie lese ich eine Webseite in Node.js als sauberes Markdown aus?

Rufe `client.v2.perceive(url, { outputs: ["markdown"] })` auf. Du bekommst in `op.outputs.markdown.url` eine 15 Minuten gültige signierte URL zum Markdown sowie einen `renderQuality`-Wert für den Lesevorgang. Willst du stattdessen direkt die Bytes, rufe `client.v2.perceiveDirect(url, { outputs: ["markdown"] })` auf und lies `result.content`.

### Was ist renderQuality und warum ist das wichtig?

`renderQuality` ist ein Wert zwischen 0.0 und 1.0, der an jedem V2-Rendering hängt. Eine Bot-Challenge, eine Login-Schranke, eine HTTP-Fehlerseite, ein Soft-404 oder eine leere SPA-Hülle erhalten alle einen niedrigen Wert und kommen mit benannten `deductions` und `warnings` zurück. So wird ein schlechter Lesevorgang markiert, statt still als echte Seite in den Kontext deines Agenten zu wandern. Werte unterhalb von etwa 0.40 bedeuten, dass das Rendering praktisch fehlgeschlagen ist, auch wenn die Anfrage 200 geliefert hat.

### Wie konvertiere ich HEIC in Node.js nach WebP?

Rufe `convertImage` mit der HEIC-Datei (ein Pfad oder ein `{ data, filename }`-Buffer-Objekt) und `outputFormat: "webp"` auf. Das SDK konvertiert zwischen `jpeg`, `png`, `svg`, `heic` und `webp`; das Eingabeformat wird aus der Dateiendung erkannt.

### Wie komprimiere ich in Node.js ein Bild, ohne das Format zu ändern?

Rufe `compressImage` mit einer `.png`-, `.jpg`-, `.jpeg`- oder `.webp`-Datei auf. Die Ausgabe behält Format und Endung der Eingabe, entfernt Metadaten und bewahrt dabei ICC-Profil und EXIF-Ausrichtung, und ist nie größer als die Eingabe. Ergänze `targetSizeKb`, um in Richtung eines Größenbudgets herunterzuskalieren; das Ziel gilt als Best Effort, lies also `result.fileSize`, um zu sehen, was tatsächlich erreicht wurde.

### Wie konvertiere ich ein beliebiges Dokument für eine RAG-Pipeline nach Markdown?

Rufe `convertToMarkdown` mit der Datei auf und übergib `saveTo`, um die `.md`-Datei direkt auf die Festplatte zu schreiben. Die Methode akzeptiert 22 Endungen aus Office, OpenDocument, PDF, EPUB, HTML, CSV und Klartext und liefert eine überschriftenbewusste Markdown-Datei zurück, sodass dein Chunker an den Überschriften des Dokuments trennen kann statt an willkürlichen Zeichenzahlen.

### Wie verwandle ich in Node.js eine ganze Website in RAG-fertige Chunks?

Rufe `client.v2.ingest({ mode: "sitemap", url, maxPages, chunk: { maxWords: 512, sentenceOverlap: 1 } })` auf. Ingest läuft immer asynchron, frage also `client.v2.getIngestJob(job.jobId)` ab, bis `status` den Wert `"completed"` hat, und lies `outputUrl` für das signierte JSONL. Alternativ setzt du `webhookUrl` und lässt dich vom Fertigstellungs-Webhook informieren. Für lokale Dokumente statt einer Website durchläuft `client.v2.ingestFiles([...])` dieselbe Pipeline.

### Wie extrahiere ich in Node.js strukturiertes JSON aus einer Seite?

Rufe `client.v2.distill({ urls, schema })` auf, wobei `schema` entweder ein JSON-Schema-Objekt oder eine flache `{ field: description }`-Zuordnung ist. Ergänze ein `cssSchema`, dann beantwortet der Selektor-Durchgang alles, was er kann, bevor irgendetwas auf die LLM-Stufe eskaliert; `result.extractionTier`, `fieldsFromCss` und `fieldsFromLlm` verraten dir, welche Stufe die Arbeit erledigt hat.

### Wie überwache ich in Node.js eine Webseite auf Änderungen?

Rufe `client.v2.createWatcher(url, { frequencyMinutes: 60, diffMode: "auto", webhookUrl })` auf. Die Stundengrenze ist hart, 60 ist also der kleinste Takt. Die Historie liest du mit `getWatcherSnapshots`, pausieren kannst du mit `updateWatcher(id, { status: "paused" })`, und entfernen mit `deleteWatcher`, was ein idempotenter Soft Delete ist.

### Wie geht das SDK mit langen Konvertierungen um, die ins Reverse-Proxy-Timeout laufen?

Vor jeder V1-Konvertierungsanfrage erzeugt das SDK eine UUID und sendet sie als `job_id`; liefert die Anfrage 5xx, fragt es still alle 3 Sekunden `GET /v1/convert/status/{job_id}` ab, bis der Job `success` oder `failed` meldet. Die Polling-Frist beträgt 5 Minuten, danach wirft es `APIError(504, "Conversion timed out")`. V2 nutzt stattdessen explizite Job-IDs, die mit `getPerceiveBatch` oder `getIngestJob` abgefragt werden.

### Kann ich das Node.js-SDK in einer Browser-Anwendung verwenden?

Nein, das SDK ist ausschließlich serverseitig, weil es sich mit einem privaten API-Schlüssel (`sk_...`) authentifiziert, der niemals in clientseitigen Code gebündelt werden darf. Es läuft in Node 18+, Bun und Deno über den npm-Spezifizierer.

### Wie lange ist die vorsignierte Download-URL gültig?

Die `presignedUrl` in jedem `ConversionResult` ist eine Stunde lang gültig. V2-Artefakt-URLs gelten 15 Minuten und werden bei jedem Lesen neu signiert, `getPerceiveOperation(operationId)` liefert dir also frische Links. Für dauerhaften Zugriff lade die Datei herunter und lege sie in deinem eigenen Bucket ab.
