---
seo_title: Rust SDK für Dateikonvertierung: crates.io-Client | EnConvert
meta_desc: Offizielles EnConvert Rust SDK: ein blockierender reqwest-Client ohne Async-Runtime, für Dateikonvertierung und den kompletten V2-Namespace für Web-Intelligence.
keywords: rust sdk dateikonvertierung, dateien konvertieren rust, url zu pdf rust, rust web scraping api, docx in pdf konvertieren rust, enconvert rust sdk, html zu pdf rust crate, heic in webp konvertieren rust, website screenshot api rust, blockierender reqwest client, markdown aus url extrahieren rust
---

# Rust SDK für Dateikonvertierung

`enconvert` ist die offizielle Rust-Crate für die EnConvert-API. Sie ist ein blockierender Client auf Basis der Blocking-API von `reqwest`: keine Async-Runtime, kein `tokio` in deinem Abhängigkeitsbaum und nirgends ein `.await` in deinem Code. Zwölf Methoden auf `Enconvert` decken die Datei-Konvertierung ab: URL zu PDF, Screenshots, Markdown-Extraktion, Bild- und Dokumentformatpaare, Anything-to-PDF und Batches für ganze Websites. Ein zweiter Namespace, `client.v2()`, ergänzt dreiundzwanzig Methoden für Web-Intelligence: Perceive, Discover, Lookup, Distill, Ingest und Watch. Jedes Options-Struct leitet `Default` ab, und jeder Fehler landet in einem einzigen Error-Enum.

<div class="alert alert-info">
<strong>crates.io:</strong> <code>enconvert</code> (0.1.0) &middot; <strong>Quelle:</strong> <a href="https://github.com/conversionapi/rust-sdk">conversionapi/rust-sdk</a> &middot; <strong>Rust:</strong> Edition 2021 &middot; <strong>Lizenz:</strong> MIT
</div>

---

## Installation

```bash
cargo add enconvert
```

Oder trage die Crate von Hand ein. `distill` und die `schema`-Extraktion von Perceive nehmen eine `serde_json::Map<String, Value>` entgegen, ergänze also auch `serde_json`, wenn du sie nutzen möchtest:

```toml
[dependencies]
enconvert = "0.1"
serde_json = "1"
```

Die Crate zieht `reqwest` (blocking, json, multipart), `serde`, `serde_json`, `thiserror` und `uuid` mit herein. Es gibt keine optionalen Features zu aktivieren.

---

## Schnellstart

```rust
use enconvert::{Enconvert, PerceiveOptions, PerceiveOutputName, UrlToPdfOptions};

fn main() -> Result<(), enconvert::Error> {
    let key = std::env::var("ENCONVERT_API_KEY").expect("ENCONVERT_API_KEY is not set");
    let client = Enconvert::new(key)?;

    // Eine URL in ein PDF konvertieren und das Ergebnis direkt auf die Platte schreiben.
    let pdf = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
        save_to: Some("page.pdf".into()),
        ..Default::default()
    })?;
    println!("{}", pdf.presigned_url);

    // Dieselbe Seite so lesen, wie es dein Agent tun sollte, mit angehängtem Qualitätswert.
    let op = client.v2().perceive("https://example.com", PerceiveOptions {
        outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
        ..Default::default()
    })?;
    println!("{:?}", op.render_quality); // z. B. Some(0.93)
    Ok(())
}
```

Jede Methode blockiert den aufrufenden Thread. Damit lässt sich das SDK mühelos in ein CLI, ein Build-Skript, einen Worker-Thread oder einen synchronen Web-Handler einsetzen. Alle öffentlichen Typen werden am Crate-Root re-exportiert, die Beispiele unten brauchen also nur `use enconvert::{...}` und den `client` von oben.

<div class="alert alert-warning">
<strong>Rufe das SDK nicht aus einem Thread einer Async-Runtime auf.</strong> Der blockierende Client von <code>reqwest</code> lässt sich nicht aus einem Thread heraus betreiben, der bereits einem Tokio-Reaktor (oder einem vergleichbaren) gehört. Aus Async-Code heraus verpackst du jeden Aufruf in <code>tokio::task::spawn_blocking</code>.
</div>

---

## Was der Client bereitstellt

| Bereich | Erreichbar über | Was er abdeckt |
|---------|-----------|----------------|
| Datei-Konvertierung | `client.<method>` | 12 Methoden: URL zu PDF, Screenshot und Markdown; Bild- und Dokumentpaare; Anything-to-Markdown und Anything-to-PDF; Batches für ganze Websites; Job- und Batch-Status |
| Web-Intelligence | `client.v2().<method>` | 23 Methoden über Perceive, Discover, Lookup, Distill, Ingest und Watch |
| Formattabellen | `valid_outputs_for`, `IMPLEMENTED_CONVERSIONS` | Die 43 implementierten `{input}-to-{output}`-Endpunkte, clientseitig geprüft, bevor eine Anfrage rausgeht |
| Fehler | `enconvert::Error` | Ein Enum, neun Varianten, mit `is_authentication`, `is_quota`, `is_rate_limit`, `is_server_error` und `status_code` |

Nichts ist ein Builder und nichts ist `async`. Die idiomatische Aufrufform ist ein Struct-Literal mit `..Default::default()`.

---

## Datei-Konvertierung

### `convert_url_to_pdf`

Rendere jede öffentliche URL zu einem PDF.

```rust
use enconvert::{PdfOptions, PdfOrientation, UrlToPdfOptions};

let result = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
    single_page: Some(false),
    pdf_options: Some(PdfOptions {
        page_size: Some("A4".to_string()),
        orientation: Some(PdfOrientation::Landscape),
        ..Default::default()
    }),
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;
println!("{} ({:?} bytes)", result.filename, result.file_size);
```

| Feld | Typ | Standard | Beschreibung |
|-------|------|---------|-------------|
| `save_to` | `Option<PathBuf>` | - | Lokaler Pfad, unter dem das PDF geschrieben wird. Übergeordnete Verzeichnisse werden automatisch angelegt. |
| `single_page` | `Option<bool>` | `true` | `true` erzeugt eine einzige durchgehende Seite. `false` paginiert anhand von `pdf_options.page_size`. |
| `pdf_options` | `Option<PdfOptions>` | - | Seitengeometrie, Skalierung, Graustufen, Kopf- und Fußzeile. Siehe [PDF-Optionen](#pdf-optionen). |
| `render.viewport_width` / `render.viewport_height` | `Option<u32>` | `1920` / `1080` | Browser-Viewport in Pixeln. |
| `render.load_media` / `render.enable_scroll` | `Option<bool>` | `true` | Auf Bilder und Videos warten und von oben nach unten scrollen, damit Lazy Loader auslösen. |
| `render.output_filename` | `Option<String>` | automatisch | Überschreibt den generierten Dateinamen. |
| `render.auth` / `render.cookies` / `render.headers` | siehe Typen | - | HTTP Basic Auth, bis zu 50 eingeschleuste Cookies, bis zu 20 zusätzliche Request-Header. |

Der `render`-Block ist `UrlRenderOptions` und wird von jeder URL- und Website-Konvertierung geteilt. Kombiniere `auth` nicht mit einem `Authorization`-Header: Die API weist diesen Konflikt zurück.

### `convert_url_to_screenshot`

Nimm ein PNG von einer beliebigen URL auf. `UrlToScreenshotOptions` trägt denselben `render`-Block plus `save_to`, und sonst nichts.

```rust
use enconvert::{UrlRenderOptions, UrlToScreenshotOptions};

client.convert_url_to_screenshot("https://example.com", UrlToScreenshotOptions {
    render: UrlRenderOptions { viewport_width: Some(1440), ..Default::default() },
    save_to: Some("shot.png".into()),
})?;
```

### `convert_url_to_markdown`

Extrahiere sauberes GitHub-Flavored Markdown aus einer URL. Navigation, Fußzeilen, Werbung und Skripte werden entfernt, der eigentliche Artikeltext bleibt erhalten, und ein YAML-Frontmatter (Titel, Beschreibung, URL, Links, Bilder) wird vorangestellt.

```rust
use enconvert::UrlToMarkdownOptions;

client.convert_url_to_markdown("https://example.com/article", UrlToMarkdownOptions {
    save_to: Some("article.md".into()),
    ..Default::default()
})?;
```

Nützlich für RAG-Pipelines, CMS-Importe und das Sammeln von Trainingsdaten. Für einen bewerteten Lesevorgang mit Artefakten und strukturierter Extraktion in einem einzigen Aufruf nimmst du stattdessen [Perceive](#perceive).

### `convert_image`

Konvertiere zwischen `jpeg`, `png`, `svg`, `heic` und `webp` (alle 20 geordneten Paare) oder rastere ein PDF nach JPEG.

```rust
use enconvert::{ConvertImageOptions, NamedFile};

// Aus einem Pfad: Das Eingabeformat ergibt sich aus der Dateiendung.
client.convert_image("photo.heic", ConvertImageOptions {
    output_format: "webp".to_string(),
    save_to: Some("photo.webp".into()),
    ..Default::default()
})?;

// Aus Bytes: Gib einen Dateinamen an, damit die Endung weiterhin lesbar ist.
let data = std::fs::read("photo.heic")?;
client.convert_image(
    NamedFile { data, filename: "photo.heic".to_string(), content_type: None },
    ConvertImageOptions { output_format: "png".to_string(), ..Default::default() },
)?;
```

`output_format` ist das einzige Pflichtfeld; `save_to` und `output_filename` sind optional. Das erste Argument ist alles, was sich in `FileInput` umwandeln lässt: ein `&str`- oder `String`-Pfad, ein `&Path` oder `PathBuf`, ein `Vec<u8>` oder `&[u8]` mit Rohbytes, oder ein `NamedFile`, wenn du Bytes hast und Dateiname und Content-Type selbst festlegen willst.

### `convert_document`

Konvertiere Dokumente und Datenformate. `output_format` ist standardmäßig `"pdf"`, und `yml`, `htm`, `md` sowie `jpg` werden auf ihre kanonischen Namen normalisiert. `save_to`, `output_filename` und `pdf_options` sind optional.

```rust
use enconvert::{ConvertDocumentOptions, PdfOptions};

// docx zu pdf (das Standard-Ausgabeformat)
client.convert_document("report.docx", ConvertDocumentOptions {
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

// json zu yaml
client.convert_document("data.json", ConvertDocumentOptions {
    output_format: Some("yaml".to_string()),
    save_to: Some("data.yaml".into()),
    ..Default::default()
})?;

// markdown zu pdf mit eigener Seiteneinrichtung
client.convert_document("README.md", ConvertDocumentOptions {
    pdf_options: Some(PdfOptions { page_size: Some("A4".to_string()), ..Default::default() }),
    ..Default::default()
})?;
```

**Erkannte Eingabe-Dateiendungen:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`.

| Eingabeformat | Gültige Ausgaben |
|--------------|---------------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `csv` | `json`, `xml` |
| `yaml`, `toml` | `json` |
| `markdown` | `html`, `pdf` |
| `html` | `pdf` |
| `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers` | `pdf` |
| `jpeg`, `png`, `svg`, `heic`, `webp` | jeweils untereinander, alle 20 Paare |
| `pdf` | `jpeg` |

EPUB hat kein eigenes Dokumentpaar. Schicke `.epub`-Dateien stattdessen durch `convert_to_pdf` oder `convert_to_markdown`.

`convert_image` und `convert_document` leiten das Eingabeformat beide aus der Dateiendung ab und prüfen das Paar gegen die clientseitige Tabelle. Ein nicht implementiertes Paar liefert also `Error::UnsupportedConversion` samt Liste der gültigen Ausgaben, bevor überhaupt eine HTTP-Anfrage gestellt wird. Dieselbe Tabelle kannst du direkt abfragen:

```rust
use enconvert::{valid_outputs_for, IMPLEMENTED_CONVERSIONS};

println!("{:?}", valid_outputs_for("json"));   // ["csv", "toml", "xml", "yaml"]
println!("{}", IMPLEMENTED_CONVERSIONS.len()); // 43
```

### `convert_to_markdown`

Lade ein Dokument nahezu beliebigen Typs hoch und erhalte sauberes Markdown zurück: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD sowie ältere oder ODF-Office-Dateien. Das Format wird serverseitig erkannt, es läuft also keine clientseitige Endungsprüfung. Bilder werden nicht unterstützt.

```rust
use enconvert::ConvertToMarkdownOptions;

client.convert_to_markdown("handbook.pdf", ConvertToMarkdownOptions {
    save_to: Some("handbook.md".into()),
    ..Default::default()
})?;
```

Die Ausgabe ist eine einzige überschriftenbewusste `.md`-Datei, was sie zu einem guten Baustein für RAG-Chunking macht: Ein semantischer Chunker kann anhand der Überschriftenhierarchie des Dokuments selbst trennen statt an willkürlichen Zeichenzahlen. Hier gibt es keine PDF-Optionen, nur `save_to` und `output_filename`.

### `convert_to_pdf`

Lade fast alles hoch und erhalte ein PDF zurück: Office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, reinen Text, Rasterbilder, SVG, EPUB oder ein bestehendes PDF zum Durchreichen.

```rust
use enconvert::{ConvertToPdfOptions, PdfOptions};

client.convert_to_pdf("slides.pptx", ConvertToPdfOptions {
    save_to: Some("slides.pdf".into()),
    ..Default::default()
})?;

// PDF rein, Graustufen-PDF raus.
client.convert_to_pdf("scan.pdf", ConvertToPdfOptions {
    pdf_options: Some(PdfOptions { grayscale: Some(true), ..Default::default() }),
    save_to: Some("scan-gray.pdf".into()),
    ..Default::default()
})?;
```

<div class="alert alert-warning">
<strong>Hier wird ausschließlich <code>pdf_options.grayscale</code> berücksichtigt.</strong> Der Anything-to-PDF-Endpunkt erkennt die Eingabe selbst und wendet seine eigene Geometrie an. Für Kontrolle über Seitengröße, Ausrichtung, Ränder, Skalierung, Kopf- und Fußzeilen nutzt du <code>convert_url_to_pdf</code> oder <code>convert_document</code> mit einer HTML- oder Markdown-Eingabe.
</div>

### `convert_website_to_pdf` und `convert_website_to_screenshot`

Ermittle jede Seite einer Website, konvertiere jede einzelne im Hintergrund und sammle alles in einem ZIP. Beide arbeiten ausschließlich asynchron: Sie liefern ein `BatchSubmission` zurück, nie eine fertige Datei.

```rust
use enconvert::{CrawlMode, WaitForBatchOptions, WebsiteConversionOptions, WebsiteToPdfOptions};

let batch = client.convert_website_to_pdf("https://example.com", WebsiteToPdfOptions {
    website: WebsiteConversionOptions {
        crawl_mode: Some(CrawlMode::Sitemap),
        exclude_patterns: Some(vec!["/blog/tag/".to_string()]),
        notification_email: Some("ops@example.com".to_string()),
        ..Default::default()
    },
    ..Default::default()
})?;

// Blockieren, bis der Batch durch ist, dann das ZIP speichern.
let status = client.wait_for_batch(&batch.batch_id, WaitForBatchOptions {
    save_to: Some("site.zip".into()),
    ..Default::default()
})?;
println!("{} of {} pages converted", status.completed, status.total);
```

| Feld | Typ | Standard | Beschreibung |
|-------|------|---------|-------------|
| `website.crawl_mode` | `Option<CrawlMode>` | `Auto` | `Auto`, `Sitemap` (nur sitemap.xml) oder `Full` (Sitemap plus Breitensuche-Crawl). |
| `website.include_patterns` / `website.exclude_patterns` | `Option<Vec<String>>` | - | Nur URLs crawlen, die auf diese Muster passen, oder sie überspringen. Nur im Full-Crawl-Modus. |
| `website.notification_email` / `website.callback_url` | `Option<String>` | Projektinhaber / - | Adresse, an die eine E-Mail geht, und Webhook, der aufgerufen wird, sobald der Batch fertig ist. |
| `website.render` | `UrlRenderOptions` | - | Dieselben Felder für Viewport, Medien, Scrollen, Auth, Cookies und Header wie bei Einzel-URL-Konvertierungen. |
| `single_page` / `pdf_options` | siehe oben | - | Nur für PDF-Batches. |

`convert_website_to_screenshot` nimmt `WebsiteToScreenshotOptions` entgegen, ein Typalias für `WebsiteConversionOptions`, und erzeugt ein ZIP mit PNGs. `wait_for_batch` fragt `get_batch_status` alle `interval_ms` ab (Standard 5000), bis der Batch den Zustand `Processing` verlässt, und lädt anschließend optional das ZIP herunter. Läuft vorher `timeout_ms` ab (Standard 1.800.000, also 30 Minuten), kommt `Error::Api { status: 504, .. }` zurück.

### `get_job_status` und `get_batch_status`

```rust
use enconvert::JobStatusValue;

let status = client.get_job_status("job_abc123")?;
match status.status {
    JobStatusValue::Success => println!("{:?}", status.presigned_url),
    JobStatusValue::Failed => eprintln!("{:?}", status.error),
    other => println!("still running: {other:?}"),
}

let batch = client.get_batch_status("batch_abc123")?; // aggregierte Zähler und Einträge pro URL
println!("{}/{} done, {} failed", batch.completed, batch.total, batch.failed);
```

<div class="alert alert-info">
<strong>Du brauchst <code>get_job_status</code> selten direkt.</strong> Das SDK fragt ihn für dich ab, wenn eine synchrone Konvertierung mit 5xx antwortet. Siehe <a href="#timeout-recovery">Timeout-Recovery</a>.
</div>

---

## Web-Intelligence (V2)

Alles unter `client.v2()` verwandelt Webseiten in agentenfertige Daten. Was jeder Lesevorgang gemeinsam hat, ist `render_quality`, ein Wert von 0.0 bis 1.0, der an jede gerenderte Seite angehängt wird. Ein niedriger Wert bedeutet, dass die Seite nicht sauber gerendert hat: eine Challenge-Seite, eine Cookie- oder Login-Wall, eine leere SPA-Hülle oder ein HTTP-Fehler. Der Inhalt kommt trotzdem zurück, aber markiert, mit einer benannten `deductions`-Map und einer `warnings`-Liste, sodass ein schlechter Lesevorgang nie unbemerkt in den Kontext deines Agenten rutscht. Ergebnisse von Perceive, Distill, Lookup und Watch tragen den Wert alle. Beginne mit der [V2-Übersicht](/de/docs/v2-overview) für die Konzepte hinter den sechs Fähigkeiten.

### Perceive

Rendere eine URL in genau die Artefakte, die du anforderst. Synchron: Der Aufruf liefert eine abgeschlossene Operation mit 15 Minuten gültigen signierten Artefakt-URLs zurück. Siehe [Perceive](/de/docs/v2-perceive).

```rust
use enconvert::{PerceiveExtractName, PerceiveOptions, PerceiveOutputName};

let op = client.v2().perceive("https://example.com", PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Screenshot]),
    extract: Some(vec![PerceiveExtractName::Tables, PerceiveExtractName::Metadata]),
    ..Default::default()
})?;

println!("{:?}", op.render_quality);
println!("{:?}", op.deductions); // z. B. {"http_error": 0.7}, wenn etwas nicht stimmt
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref()));
println!("{:?}", op.structured);

// Artefakt-URLs werden bei jeder Statusabfrage neu signiert.
client.v2().get_perceive_operation(&op.operation_id)?;
```

| Option | Typ | Standard | Beschreibung |
|--------|------|---------|-------------|
| `outputs` | `Option<Vec<PerceiveOutputName>>` | `markdown`, `structured` | `Markdown`, `HtmlCleaned`, `HtmlRaw`, `Screenshot`, `ScreenshotFullPage`, `Pdf`, `Links`, `Images`, `Structured`. |
| `extract` | `Option<Vec<PerceiveExtractName>>` | - | `Tables`, `Prices`, `Contacts`, `Metadata`, `MainContent`, `Headings`, `StructuredData`, `Technologies`, `All`. |
| `schema` | `Option<Map<String, Value>>` | - | JSON-Schema für die strukturierte Extraktion. |
| `only_main_content` | `Option<bool>` | `true` | Entfernt Navigation, Header, Footer und Cookie-Banner aus dem Markdown-Artefakt und dem `main_content`-Extrakt. |
| `wait_for` / `wait_timeout_ms` / `js_code` | siehe Typen | - / `30000` / - | Ein CSS-Selektor (optional `css:...`) oder `js:<expr>`, auf den gewartet wird, die Wartedauer (0 bis 60000) und JavaScript, das nach der Navigation ausgeführt wird (max. 20000 Zeichen). |
| `viewport` / `mobile` | `Option<PerceiveViewport>` / `Option<bool>` | 1920x1080 | Breite 320 bis 3840, Höhe 240 bis 2160, oder ein Mobilgeräte-Profil. |
| `cache_mode` / `block_resources` | siehe Typen | `Enabled` / - | `Enabled` cacht für 1 Stunde, `Bypass` überspringt den Cache, `Refresh` rendert neu; dazu die Ressourcentypen, die der Browser nicht laden soll. |
| `headers`, `cookies`, `auth`, `respect_robots`, `pdf_options` | siehe `UrlRenderOptions` und `PdfOptions` | - | Dieselben Formen wie bei den V1-Render-Optionen. `pdf_options` spielt nur eine Rolle, wenn `outputs` `Pdf` enthält. |

`perceive_direct` streamt ein einzelnes Artefakt als Rohbytes zurück statt eines JSON-Envelopes. Es muss genau eine artefakterzeugende Ausgabe angefordert werden, und das SDK erzwingt das lokal mit `Error::InvalidInput`, bevor irgendetwas gesendet wird.

```rust
use enconvert::{PerceiveOptions, PerceiveOutputName};

let direct = client.v2().perceive_direct("https://example.com", PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Pdf]),
    ..Default::default()
})?;
std::fs::write(direct.filename.as_deref().unwrap_or("page.pdf"), &direct.content)?;

// Ein gespeichertes Artefakt später erneut laden. Übergib None, wenn die Operation nur eines erzeugt hat.
let saved = client
    .v2()
    .download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;
println!("{} bytes", saved.content.len());
```

`PerceiveDirectResult` trägt `content`, `content_type`, `filename`, `operation_id`, `object_key`, `cache_hit`, `render_quality`, `source_status_code`, `content_hash` und `warnings_count`, alle aus den Response-Headern gelesen. `Error::Api { status: 410, .. }` von `download_perceive_artifact` bedeutet, dass das Artefakt seine Aufbewahrungsfrist überschritten hat.

`perceive_batch` nimmt bis zu 1000 URLs und einen gemeinsamen Optionsblock. Kleine Batches laufen inline; größere kommen mit Status `Queued` zurück, frage sie also über `get_perceive_batch` mit der `job_id` ab.

```rust
use enconvert::{PerceiveBatchOptions, PerceiveBatchOutputMode, PerceiveOptions, PerceiveOutputName};

let render = PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Markdown]),
    ..Default::default()
};
let batch = client.v2().perceive_batch(
    vec!["https://example.com/a".to_string(), "https://example.com/b".to_string()],
    PerceiveBatchOptions { render, output_mode: Some(PerceiveBatchOutputMode::Zip) },
)?;

let done = client.v2().get_perceive_batch(&batch.job_id)?;
for item in &done.items {
    println!("{} {:?}", item.url, item.render_quality);
}
```

### Discover

Zähle die URLs einer Website auf, ganz ohne Browser-Rendering. Siehe [Discover](/de/docs/v2-discover).

```rust
use enconvert::{DiscoverMode, DiscoverOptions};

let found = client.v2().discover("https://example.com", DiscoverOptions {
    mode: Some(DiscoverMode::Hybrid),
    max_urls: Some(200),
    exclude_patterns: Some(vec!["/tag/".to_string()]),
    ..Default::default()
})?;
println!("{} urls, truncated: {}", found.total, found.truncated);
```

`mode` ist standardmäßig `Hybrid` (`Sitemap` und `Crawl` sind die Alternativen), `max_urls` steht auf 100 (1 bis 1000), `max_depth` auf 2 (1 bis 5) und `same_domain_only` auf true. `include_patterns` und danach `exclude_patterns` filtern per Regex, jeweils maximal 50 Einträge, und `respect_robots` berücksichtigt robots.txt. Das Ergebnis trägt `urls`, `total`, `pages_crawled`, `truncated`, `robots_respected` und eine `sources`-Map mit den rohen Zählern je Quelle.

### Lookup

Führe eine kategorisierte Websuche aus und rendere im selben Aufruf optional die besten Treffer. Siehe [Lookup](/de/docs/v2-lookup).

```rust
use enconvert::{LookupCategory, LookupOptions};

let search = client.v2().lookup("best static site generators", LookupOptions {
    category: Some(LookupCategory::Web),
    num_results: Some(10),
    country: Some("us".to_string()),
    perceive_top: Some(3),
    ..Default::default()
})?;

for hit in &search.results {
    let quality = hit.perceive.as_ref().and_then(|p| p.render_quality);
    println!("{:?} {:?} {quality:?}", hit.title, hit.url);
}
```

`category` ist standardmäßig `Web` (`News`, `Images`, `Scholar`, `Patents` und `Maps` sind die übrigen), `num_results` steht auf 10 (1 bis 100), `page` auf 1 (1 bis 10) und `autocorrect` auf true. `country` und `locale` nehmen Codes wie `"us"` und `"en"`, `location` nimmt freien Text wie `"Austin, Texas"`, und `time_filter` akzeptiert `Hour`, `Day`, `Week`, `Month` oder `Year`. `perceive_top` (0 bis 10, Standard 0) rendert automatisch die obersten N Ergebnis-URLs und hängt jedem Treffer ein vollständiges `PerceiveResult` an. Das Ergebnis trägt außerdem `answer_box`, `knowledge_graph` und `perceive_operation_ids`.

### Distill

Schemagesteuerte strukturierte Extraktion: Du gibst eine Form vor und bekommst genau diese Form für jede URL zurück. Optional läuft zuerst ein CSS-Durchgang, und alles, was er verfehlt, eskaliert an die LLM-Stufe. Siehe [Distill](/de/docs/v2-distill).

```rust
use enconvert::{CssField, CssFieldType, CssSchema, DistillOptions};
use serde_json::json;

// CssField hat kein Default, weil `field_type` erforderlich ist.
fn text_field(name: &str, selector: &str) -> CssField {
    CssField {
        name: name.into(), field_type: CssFieldType::Text, selector: Some(selector.into()),
        attribute: None, pattern: None, default: None, transform: None, fields: None,
    }
}

let distilled = client.v2().distill(DistillOptions {
    urls: Some(vec!["https://example.com/pricing".to_string()]),
    schema: json!({ "plans": "list of plan names with monthly prices" })
        .as_object().unwrap().clone(),
    css_schema: Some(CssSchema {
        base_selector: ".plan-card".to_string(),
        fields: vec![text_field("name", "h3"), text_field("price", ".price")],
        name: None,
        target_field: None,
    }),
    ..Default::default()
})?;

for item in &distilled.results {
    println!("{:?} tier {:?} css {} llm {}",
        item.data, item.extraction_tier, item.fields_from_css, item.fields_from_llm);
}
```

Oder ermittle zuerst die Seiten einer Website und destilliere jede gefundene Seite:

```rust
use enconvert::{DistillDiscoverFrom, DistillOptions};
use serde_json::json;

client.v2().distill(DistillOptions {
    discover_from: Some(DistillDiscoverFrom {
        max_pages: Some(20), // 1 bis 50, begrenzt Discovery und Distillation zugleich
        ..DistillDiscoverFrom::new("https://example.com")
    }),
    schema: json!({ "title": "page title" }).as_object().unwrap().clone(),
    ..Default::default()
})?;
```

Gib genau eines von beiden an: `urls` (maximal 50) oder `discover_from`. Beides oder keines von beidem liefert `Error::InvalidInput`, bevor eine Anfrage rausgeht. `schema` ist erforderlich und ist entweder ein JSON-Schema-Objekt oder eine flache `{field: description}`-Map. `CssFieldType` deckt `Text`, `Attribute`, `Html`, `Regex`, `Nested`, `List` und `NestedList` ab; `transform` akzeptiert `Lowercase`, `Uppercase` oder `Strip`; die Verschachtelung ist auf Tiefe 5 begrenzt.

### Ingest

Verwandle eine ganze Website oder einen Stapel hochgeladener Dokumente über eine einzige Pipeline in gechunktes, RAG-fertiges JSONL. Ingest arbeitet immer asynchron. Siehe [Ingest](/de/docs/v2-ingest).

```rust
use enconvert::{
    IngestChunkOptions, IngestFilesOptions, IngestMode, IngestOptions, IngestStatus, V2ListOptions,
};

// Von einer Website.
let job = client.v2().ingest(IngestOptions {
    mode: Some(IngestMode::Sitemap),
    url: Some("https://docs.example.com".to_string()),
    max_pages: Some(100),
    chunk: Some(IngestChunkOptions { max_words: Some(512), sentence_overlap: Some(1) }),
    webhook_url: Some("https://my.app/hooks/enconvert".to_string()),
    ..Default::default()
})?;

// Oder aus hochgeladenen Dateien: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD,
// sowie ältere oder ODF-Office-Dokumente.
let files = vec!["handbook.pdf".into(), "notes.docx".into()];
client.v2().ingest_files(files, IngestFilesOptions::default())?;

// Abfragen, auflisten, abbrechen.
let status = client.v2().get_ingest_job(&job.job_id)?;
if status.status == IngestStatus::Completed {
    println!("{:?}", status.output_url); // signierte URL zum JSONL
}
client.v2().list_ingest_jobs(V2ListOptions { limit: Some(50), ..Default::default() })?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotent
```

`mode` steht standardmäßig auf `Urls`, was eine nicht leere `urls`-Liste (maximal 1000) verlangt und `url` ablehnt; `Sitemap` und `Crawl` verhalten sich umgekehrt und benötigen die Start-`url`. Diese Kopplung wird clientseitig geprüft, eine Fehlpaarung liefert also sofort `Error::InvalidInput`. `max_pages` steht standardmäßig auf 50 (1 bis 1000), `max_depth` auf 2 (1 bis 5), `same_domain_only` auf true, `chunk.max_words` auf 512 (32 bis 4000) und `chunk.sentence_overlap` auf 1 (0 bis 10). `include_patterns`, `exclude_patterns`, `respect_robots`, `wait_for` und `wait_timeout_ms` verhalten sich wie bei Discover und Perceive. Abschluss-Webhooks sind HMAC-signiert:

```rust
let secret = client.v2().get_webhook_secret()?;
println!("{} {} {}", secret.secret, secret.signature_header, secret.signature_scheme);
client.v2().rotate_webhook_secret()?;            // alte Signaturen gelten sofort nicht mehr
client.v2().retry_ingest_webhook("ing_abc123")?; // Webhook eines fertigen Jobs erneut zustellen
```

### Watch

Rendere eine URL in festem Takt erneut und lass dir sagen, was sich geändert hat. Siehe [Watch](/de/docs/v2-watch).

```rust
use enconvert::{
    SnapshotListOptions, V2ListOptions, WatchCreateOptions, WatchDiffMode, WatchUpdateStatus,
    WatcherUpdate,
};

let watcher = client.v2().create_watcher("https://example.com/pricing", WatchCreateOptions {
    frequency_minutes: Some(60),
    diff_mode: Some(WatchDiffMode::Auto),
    webhook_url: Some("https://my.app/hooks/changes".to_string()),
    notify_email: Some(true),
    ..Default::default()
})?;
println!("{} next check {:?}", watcher.watcher_id, watcher.next_check_at);

// Die Prüfhistorie lesen, neueste zuerst.
let history = client
    .v2()
    .get_watcher_snapshots(&watcher.watcher_id, SnapshotListOptions { limit: Some(10) })?;
for snap in &history.snapshots {
    println!("{} changed: {} similarity: {:?}", snap.checked_at, snap.has_changes, snap.similarity);
}
client.v2().list_watchers(V2ListOptions::default())?;
client.v2().get_watcher(&watcher.watcher_id)?;

// Pausieren. Some(String::new()) für webhook_url würde den Webhook löschen.
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate {
    status: Some(WatchUpdateStatus::Paused),
    ..Default::default()
})?;
client.v2().delete_watcher(&watcher.watcher_id)?; // Soft Delete, idempotent
```

`frequency_minutes` steht standardmäßig auf 60 und akzeptiert 60 bis 43200, die stündliche Untergrenze ist also hart. `diff_mode` steht standardmäßig auf `Auto` und akzeptiert außerdem `Text`, `Structured`, `Tables` und `Metadata`, wobei `track_fields` die Diff-Engine auf eine Teilmenge von Feldern oder Selektoren einschränkt. `notify_email` steht standardmäßig auf true und schickt dem Projektinhaber bei Änderungen eine E-Mail; `webhook_url` ergänzt einen HMAC-signierten Änderungs-Webhook.

`update_watcher` verlangt mindestens ein Feld und liefert `Error::InvalidInput` für ein `WatcherUpdate`, in dem alles `None` ist. Die `changes`-Einträge eines Snapshots enthalten nicht vertrauenswürdige Seiteninhalte, escape sie also, bevor du sie irgendwo darstellst.

---

## PDF-Optionen

`PdfOptions` wird von `convert_url_to_pdf`, `convert_document`, `convert_to_pdf` und der `Pdf`-Ausgabe von Perceive gemeinsam genutzt. Jedes Feld ist optional und wird nur gesendet, wenn es gesetzt ist.

```rust
use enconvert::{PdfHeaderFooter, PdfMargins, PdfOptions, PdfOrientation, UrlToPdfOptions};

fn block(text: &str, height: f64) -> PdfHeaderFooter {
    PdfHeaderFooter { content: Some(text.into()), height: Some(height) }
}

client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
    pdf_options: Some(PdfOptions {
        page_size: Some("A4".to_string()),
        orientation: Some(PdfOrientation::Landscape),
        margins: Some(PdfMargins {
            top: Some(10.0), bottom: Some(10.0), left: Some(15.0), right: Some(15.0),
        }),
        scale: Some(0.9),
        header: Some(block("Quarterly Report", 15.0)),
        footer: Some(block("Confidential", 12.0)),
        ..Default::default()
    }),
    ..Default::default()
})?;
```

| Feld | Typ | Beschreibung |
|-------|------|-------------|
| `page_size` | `Option<String>` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` und so weiter. |
| `page_width` / `page_height` | `Option<f64>` | Eigene Maße. Gemeinsam gesetzt überschreiben sie `page_size`. |
| `orientation` | `Option<PdfOrientation>` | `Portrait` oder `Landscape`. |
| `margins` | `Option<PdfMargins>` | `top`, `bottom`, `left`, `right`, jeweils optional. |
| `scale` | `Option<f64>` | Render-Skalierung, zum Beispiel `0.9` für 90 Prozent. |
| `grayscale` | `Option<bool>` | Wandelt das PDF nachträglich in Graustufen um. |
| `header` / `footer` | `Option<PdfHeaderFooter>` | `content` (max. 2000 Zeichen) und `height`. |

Die vollständige Semantik aller Parameter findest du unter [Parameter und Optionen](/de/docs/parameters-options).

---

## Fehlerbehandlung

Es gibt genau einen Fehlertyp, `enconvert::Error`. Er implementiert `std::error::Error` über `thiserror`, `?` propagiert also in jedes `Box<dyn Error>` oder `anyhow::Result`.

```rust
use enconvert::{Error, UrlToPdfOptions};

match client.convert_url_to_pdf("https://example.com", UrlToPdfOptions::default()) {
    Ok(result) => println!("{}", result.presigned_url),
    Err(e) if e.is_authentication() => eprintln!("check ENCONVERT_API_KEY"),
    Err(e) if e.is_rate_limit() => eprintln!("too many requests, back off"),
    Err(e) if e.is_server_error() => eprintln!("gateway problem, retry later"),
    Err(Error::UnsupportedConversion(msg)) => eprintln!("{msg}"),
    Err(Error::Api { status, message }) => eprintln!("API error [{status}]: {message}"),
    Err(e) => return Err(e),
}
```

| Variante | Ausgelöst bei | Statuscode |
|---------|-------------|-------------|
| `Error::Authentication(String)` | Ungültiger, fehlender oder widerrufener API-Key | `401`, `403` |
| `Error::Quota(String)` | Wird bei HTTP 402 ausgelöst | `402` |
| `Error::RateLimit(String)` | Rate-Limit überschritten | `429` |
| `Error::Api { status, message }` | Jede andere 4xx- oder 5xx-Antwort | der tatsächliche Code |
| `Error::UnsupportedConversion(String)` | Ein `{input}-to-{output}`-Paar, das die API nicht implementiert, clientseitig abgefangen | - |
| `Error::InvalidInput(String)` | Fehlerhafte Argumente, vor der Anfrage abgefangen, etwa ein leerer API-Key | - |
| `Error::Http(reqwest::Error)` | Verbindungs-, TLS-, Timeout- oder Antwort-Decodierungsfehler | vom Transport, sofern vorhanden |
| `Error::Io(std::io::Error)` | Lesen einer hochzuladenden Datei oder Schreiben eines Downloads | - |
| `Error::Json(serde_json::Error)` | Serialisieren eines Request-Bodys | - |

Vier Prädikate halten die Match-Arme kurz: `is_authentication()`, `is_quota()`, `is_rate_limit()` und `is_server_error()`. `status_code()` liefert `Option<u16>` für jede Variante, die einen Code trägt. Die vollständige Zuordnung der Meldungen steht in der [Fehlercode-Referenz](/de/docs/error-codes).

---

## Timeout-Recovery

Lange URL-zu-PDF-Renderings und große Dokumentkonvertierungen können ein Reverse-Proxy-Timeout überdauern, selbst wenn die Konvertierung auf dem Server erfolgreich ist. Das SDK fängt sich von allein wieder:

1. Vor jeder Anfrage erzeugt es eine UUID und sendet sie als `job_id` im Body oder im Multipart-Formular.
2. Kommt diese Anfrage als 5xx-`Error::Api` zurück, wechselt das SDK still auf das Abfragen von `GET /v1/convert/status/{job_id}` alle 3 Sekunden.
3. Sobald der Job `success` meldet, gibt es das Ergebnis zurück. Sobald er `failed` meldet, gibt es `Error::Api` mit der Meldung des Servers zurück.
4. Die Abfragefrist beträgt 5 Minuten. Danach bekommst du `Error::Api { status: 504, message: "Conversion timed out" }`.

Die Recovery greift bei `convert_url_to_pdf`, `convert_url_to_screenshot`, `convert_url_to_markdown`, `convert_image`, `convert_document`, `convert_to_markdown` und `convert_to_pdf`. Bewusst ausgenommen sind die Website-Batch-Einreichungen: Sie haben keine Job-Zeile, ein 5xx bedeutet dort also, dass die Einreichung selbst fehlgeschlagen ist, und das kommt direkt durch. Auch V2-Endpunkte antworten direkt und haben keinen Job-Fallback. Antworten ohne `job_id` bekommen die clientseitig erzeugte nachgetragen, `result.job_id` ist also immer etwas, das du später an `get_job_status` übergeben kannst.

---

## Konfiguration

```rust
use enconvert::Enconvert;
use std::time::Duration;

let client = Enconvert::with_options(
    std::env::var("ENCONVERT_API_KEY").expect("ENCONVERT_API_KEY is not set"),
    Some("https://api.enconvert.com"), // Basis-URL überschreiben
    Some(Duration::from_secs(300)),    // Anfrage-Timeout
)?;
println!("enconvert {}", enconvert::VERSION);
```

| Argument | Typ | Standard | Beschreibung |
|----------|------|---------|-------------|
| `api_key` | `impl Into<String>` | erforderlich | Privater API-Key. Ein leerer String liefert `Error::InvalidInput`, ebenso ein Key mit ungültigen Header-Zeichen. |
| `base_url` | `Option<&str>` | `https://api.enconvert.com` | Abschließende Schrägstriche werden entfernt. |
| `timeout` | `Option<Duration>` | 300 Sekunden | Gilt für die gesamte Anfrage. |

`Enconvert::new(api_key)` ist die Kurzform von `with_options(api_key, None, None)`.

<div class="alert alert-warning">
<strong>Schreibe den API-Key niemals fest in den Code.</strong> Lies ihn aus einer Umgebungsvariable oder einem Secret-Manager. Das SDK markiert den Wert des <code>X-API-Key</code>-Headers als sensibel, damit er nicht in Debug-Ausgaben landet, und lädt signierte URLs über einen zweiten, nicht authentifizierten HTTP-Client herunter, sodass dein Key nie an den Objektspeicher geht. Keys erzeugst und rotierst du im <a href="/de/dashboard">Dashboard</a>, und die Key-Typen erklärt die <a href="/de/docs/authentication">Authentifizierung</a>.
</div>

---

## Ergebnisform

Jede Einzeldatei-Konvertierungsmethode liefert ein `ConversionResult`:

```rust
pub struct ConversionResult {
    pub presigned_url: String,
    pub object_key: String,
    pub filename: String,
    pub file_size: Option<u64>,
    pub conversion_time_seconds: Option<f64>,
    pub job_id: Option<String>,
}
```

Lade die Datei selbst von `presigned_url` herunter, oder übergib `save_to` und lass sie das SDK für dich schreiben. Signierte URLs laufen ab, speichere also alles, was du behalten willst, in deinem eigenen Bucket.

Die asynchronen Pfade liefern eigene Formen. `JobStatus` trägt `status` (`Processing`, `Success`, `Failed` oder `Unknown(String)`), `presigned_url`, `object_key` und `error`. `BatchSubmission` trägt `batch_id`, `status`, `url_count`, `total_discovered`, `discovery_method` und `output_format`. `BatchStatus` ergänzt die Zähler `total`, `completed`, `failed` und `in_progress` sowie `output_mode`, `zip_download_url` und ein `Vec<BatchItem>` mit einer Zeile pro URL.

V2-Lesevorgänge liefern `PerceiveResult`, dessen `outputs`-Map nach Ausgabenamen (`"markdown"`, `"screenshot_full_page"` und so weiter) mit Werten vom Typ `V2OutputArtifact { url, object_key, size_bytes, content_type, expires_in }` geschlüsselt ist. Diese Artefakt-URLs sind 15 Minuten lang signiert und werden bei jedem `get_perceive_operation`-Aufruf neu signiert. Daneben stehen `render_quality`, `status_code`, `deductions`, `cache_hit`, `structured`, `extraction_tier`, `tokens`, `cost_cents`, `duration_ms`, `warnings` und `options_echo`. Jedes Antwort-Enum trägt eine `Unknown(String)`-Variante, sodass ein Statuswert, der serverseitig nach dem Build deiner Anwendung hinzukam, sauber geparst wird statt zu scheitern.

---

## Quelle und Issues

- **crates.io:** [enconvert](https://crates.io/crates/enconvert)
- **GitHub:** [conversionapi/rust-sdk](https://github.com/conversionapi/rust-sdk)
- **Lizenz:** MIT
- **Weitere Clients:** [alle SDKs](/de/docs/sdks) und die [REST-Endpunkt-Referenz](/de/docs/endpoints-overview)

---

## Häufig gestellte Fragen

### Wie konvertiere ich Dateien in Rust mit einem crates.io-Paket?

Führe `cargo add enconvert` aus, baue einen Client mit `Enconvert::new(api_key)?` und rufe eine Methode wie `convert_url_to_pdf`, `convert_image` oder `convert_document` auf. Jedes Options-Struct leitet `Default` ab, du schreibst also ein Struct-Literal mit `..Default::default()` und setzt nur die Felder, die dich interessieren. Übergib `save_to`, damit das SDK die Ausgabe direkt auf die Platte schreibt.

### Braucht das Rust SDK tokio oder eine Async-Runtime?

Nein. Es basiert auf der Blocking-API von `reqwest`, jede Methode blockiert also den aufrufenden Thread, und es gibt kein `.await`, keinen Executor und kein `tokio` in deinem Abhängigkeitsbaum. Ist deine Anwendung bereits async, rufe das SDK aus `tokio::task::spawn_blocking` auf statt direkt aus einem Reaktor-Thread, denn der blockierende Client von `reqwest` kann nicht innerhalb eines Async-Runtime-Kontexts laufen.

### Wie konvertiere ich eine URL in Rust in ein PDF?

Rufe `convert_url_to_pdf("https://example.com", UrlToPdfOptions { save_to: Some("page.pdf".into()), ..Default::default() })` auf. Setze `single_page: Some(false)`, um zu paginieren statt eine einzige durchgehende Seite zu erzeugen, und übergib `pdf_options` für Seitengröße, Ausrichtung, Ränder, Skalierung, Graustufen, Kopf- und Fußzeilen.

### Wie konvertiere ich DOCX in Rust nach PDF?

Rufe `convert_document("report.docx", ConvertDocumentOptions { save_to: Some("report.pdf".into()), ..Default::default() })` auf. PDF ist die Standardausgabe, `output_format` kann also ungesetzt bleiben. Dieselbe Methode verarbeitet XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML und Markdown nach PDF sowie Datenformatpaare wie JSON zu YAML und CSV zu XML.

### Wie konvertiere ich HEIC in Rust nach WebP?

Rufe `convert_image("photo.heic", ConvertImageOptions { output_format: "webp".to_string(), ..Default::default() })` auf. Das Eingabeformat ergibt sich aus der Dateiendung, und alle 20 geordneten Paare unter `jpeg`, `png`, `svg`, `heic` und `webp` sind implementiert, dazu `pdf` zu `jpeg` für die Rasterung. Ein nicht unterstütztes Paar liefert `Error::UnsupportedConversion`, bevor irgendein Netzwerkaufruf stattfindet.

### Wie hole ich eine Webseite aus Rust als sauberes Markdown?

Auf zwei Wegen. `convert_url_to_markdown` liefert eine einzelne Markdown-Datei mit YAML-Frontmatter. `client.v2().perceive(url, PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() })` liefert denselben Inhalt mit einem `render_quality`-Wert, benannten Abzügen, Warnungen und optional Screenshots, Links oder strukturierter Extraktion aus demselben Rendering. Nimm Perceive, wenn ein Agent das Ergebnis lesen wird.

### Woran erkenne ich, ob eine Seite tatsächlich gerendert hat?

Lies `render_quality`, einen Wert von 0.0 bis 1.0, der bei jedem V2-Lesevorgang vorhanden ist. Ein niedriger Wert bedeutet, dass das Rendering nicht sauber war: eine Challenge-Seite, eine Cookie- oder Login-Wall, eine leere SPA-Hülle oder ein HTTP-Fehler. Die `deductions`-Map benennt jeden ausgelösten Abzug, `status_code` liefert den HTTP-Status der Gegenstelle, und `warnings` listet auf, was schiefgelaufen ist. Der Inhalt wird trotzdem zurückgegeben, nur eben markiert.

### Was passiert, wenn eine Konvertierung länger dauert als das Proxy-Timeout?

Das SDK regelt das. Jede Anfrage trägt eine clientseitig erzeugte `job_id`; antwortet die Anfrage mit 5xx, fragt das SDK `GET /v1/convert/status/{job_id}` alle 3 Sekunden für bis zu 5 Minuten ab und gibt das fertige Ergebnis zurück, als wäre nichts gewesen. Nach Ablauf der Frist bekommst du `Error::Api { status: 504, message: "Conversion timed out" }`. Die Ausnahme sind Batch-Einreichungen für ganze Websites, dafür ist `wait_for_batch` das richtige Werkzeug.

### Kann ich das Rust SDK aus einem Browser oder einem WASM-Target nutzen?

Nein. Es authentifiziert sich mit einem privaten API-Key, der niemals an einen Client ausgeliefert werden darf, und es hängt am blockierenden Client von `reqwest` mit nativem TLS und Threads, die es beide auf `wasm32-unknown-unknown` nicht gibt. Betreibe es auf einem Server, in einem CLI oder in einem Worker, und lass dein Frontend mit deinem eigenen Backend sprechen.
