---
seo_title: Web Scraping API: URL zu Markdown, Screenshot & PDF | EnConvert
meta_desc: POST /v2/perceive rendert eine JavaScript-Seite in Headless-Chrome und liefert Markdown, Screenshots, PDF und strukturierte Daten aus einem Web-Scraping-API-Aufruf.
keywords: webseite in markdown umwandeln api, javascript seite rendern api, website screenshot per api erstellen, url zu markdown konvertieren, html in markdown umwandeln api, strukturierte daten von webseite extrahieren, webseite für llm auslesen, batch web scraping api
---

# Web Scraping API für Markdown, Screenshots und strukturierte Daten

`POST /v2/perceive` ist die Web-Scraping-API von EnConvert: Sie rendert
eine URL einmal in einem echten Headless-Browser (JavaScript wird
ausgeführt, Lazy-Content wird geladen) und liefert aus diesem einen
Render jede Ausgabe zurück, die du anforderst: sauberes Markdown
(standardmäßig nur der Hauptinhalt, Site-Chrome entfernt), bereinigtes
oder rohes HTML, einen Screenshot, ein PDF, das Link- und Bild-Inventar
sowie strukturierte Daten (Seitenmetadaten, JSON-LD, Überschriften,
Tabellen). Datei-Ausgaben kommen als kurzlebige, vorsignierte Download-URLs
zurück, der strukturierte Block inline, und Batches mit mehr als 10 URLs
laufen asynchron hinter einer abgefragten `job_id`. Eine einzige Anfrage
ersetzt einen ganzen Stapel einzelner Aufrufe: url-to-markdown,
url-to-screenshot, url-to-pdf, plus dein eigenes Scraping.

Hier ist der kleinste sinnvolle Aufruf. Sende eine URL und erhalte
sauberes Markdown sowie die strukturierten Metadaten der Seite zurück:

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

Die Antwort enthält eine vorsignierte Download-URL für die Markdown-Datei
sowie den strukturierten Block inline:

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

---

## Endpunkte

| Methode | Pfad | Zweck |
|--------|------|---------|
| `POST` | `/v2/perceive` | Erfasst eine einzelne URL und liefert die angeforderten Ausgaben zurück. |
| `GET` | `/v2/perceive/{operation_id}` | Ruft eine vergangene Operation mit neu signierten Download-URLs erneut ab. |
| `POST` | `/v2/perceive/batch` | Erfasst bis zu 1,000 URLs, die sich einen Satz Optionen teilen. |
| `GET` | `/v2/perceive/batch/{job_id}` | Fragt den Status und die Ergebnisse pro URL eines Batches ab. |
| `DELETE` | `/v2/perceive/batch/{job_id}` | Bricht einen laufenden Batch ab. |

**Content-Type:** `application/json` bei jedem `POST`.

---

## Authentifizierung

Authentifiziere dich mit einem privaten Schlüssel im `X-API-Key`-Header
für Server-zu-Server-Aufrufe. Diesen Weg verwenden die folgenden
Beispiele.

```http
X-API-Key: sk_your_private_key
```

Öffentliche Schlüssel mit einem JWT-Bearer-Token funktionieren ebenfalls,
nach demselben Ablauf wie bei jedem anderen Endpunkt: Erzeuge ein Token
mit deinem `pk_`-Schlüssel und sende es dann als
`Authorization: Bearer <token>`. Den vollständigen Ablauf, einschließlich
Domain-Lock und Token-Refresh, findest du im
[Authentifizierungsleitfaden](/de/docs/authentication.md).

Jeder API-Schlüssel hat eine Allowlist erlaubter Endpunkte. Steht
`/v2/perceive` nicht auf der Liste des Schlüssels, wird die Anfrage mit
`403` abgelehnt.

---

## Wie perceive funktioniert

Eine Anfrage löst einen Browser-Render über ein gemeinsam genutztes
Headless-Chrome-Singleton aus und materialisiert anschließend jede
Ausgabe aus diesem Render. Du bezahlst innerhalb eines einzelnen Aufrufs
nie zweimal für dieselbe Seite.

1. **Render.** Die Seite wird über einen automatischen
   Multi-Engine-Fallback abgerufen: zuerst ein schneller
   Echt-Browser-TLS-Fingerprint, mit Eskalation zu Headless-Chrome,
   wenn die Seite blockiert ist oder JavaScript benötigt, und noch
   einmal zu einem Stealth-gehärteten Render, wenn eine Seite weiterhin
   durch Anti-Bot-Schutz blockiert wirkt, sodass mehr reale Seiten mit
   nutzbarem Inhalt zurückkommen. Im Browser werden Cookie-Banner
   geschlossen, die Seite wird gescrollt, um Lazy-Loaded-Content
   auszulösen, Sticky-Header werden behandelt, und Bilder erhalten Zeit
   zum Laden. Es ist dieselbe Capture-Pipeline, die auch
   [den url-to-pdf-Endpunkt](/de/docs/endpoints/convert/web-pages/url-to-pdf.md)
   antreibt.
2. **Materialisieren.** Aus dem gerenderten DOM erstellt perceive alles,
   was du in `outputs` angegeben hast: Markdown, bereinigtes/rohes HTML,
   Links, Bilder, einen Screenshot, ein PDF. Das DOM wird zuvor
   normalisiert, damit das Markdown widerspiegelt, was ein Leser sieht:
   Code-Fences behalten ihre Sprache, Card-Links ihre Struktur, und
   Interface-Elemente werden unter `only_main_content` entfernt. Siehe
   [Markdown-Qualität](#markdown-qualitat).
3. **Extrahieren.** Wenn du die Ausgabe `structured` angefordert hast,
   führt perceive einen heuristischen Durchlauf für Seitenmetadaten,
   JSON-LD, Überschriften und Tabellen aus. Wenn du zusätzlich ein
   `schema` sendest und dein Plan die LLM-Stufe enthält, füllt ein
   LLM-gestütztes Modell das Schema, wenn der heuristische
   Durchlauf nicht ausreicht.
4. **Bewerten.** Ein Render-Qualitäts-Score (0.0–1.0) unterscheidet
   einen echten Render von einem fehlgeschlagenen. Werte unter 0.40
   bedeuten einen fehlgeschlagenen Render: eine Anti-Bot-Seite, eine
   Login-Wall, eine HTTP-Fehlerseite, ein Soft-404 oder eine leere
   Hülle. Sieh in `deductions` nach dem Grund und in `status_code` nach
   dem Upstream-Status.

Binäre und Text-Ausgaben (Markdown, HTML, Screenshots, PDFs, das Link-
und Bild-JSON) werden in den Storage hochgeladen und als
**vorsignierte URLs** zurückgegeben, die nach 15 Minuten ablaufen. Der
`structured`-Block wird inline im JSON zurückgegeben. Rufe eine
Operation mit `GET /v2/perceive/{operation_id}` erneut ab, um einen
neuen Satz signierter URLs zu erhalten.

---

## Request-Parameter

Die Validierung ist strikt: Ein Request-Key, den das Schema nicht kennt,
wird mit `422` abgelehnt, wobei das betroffene Feld benannt wird.
Unbekannte Keys werden nie still ignoriert. Jeder `422`-Body enthält
außerdem ein Top-Level-Array `errors` mit menschenlesbaren Meldungen
neben der maschinenlesbaren `detail`-Liste.

### Kern

| Parameter | Typ | Standard | Beschreibung |
|-----------|------|---------|-------------|
| `url` | `string` | -- | Die zu erfassende Seite. Muss mit `http://` oder `https://` beginnen. Maximal 2,048 Zeichen. Erforderlich. |
| `outputs` | `string[]` | `["markdown", "structured"]` | Welche Ausgaben erzeugt werden sollen. Siehe [Ausgaben](#outputs). |
| `extract` | `string[]` | `[]` | Welche strukturierten Felder abgerufen werden, wenn `structured` in `outputs` enthalten ist. Siehe [Strukturierte Extraktion](#strukturierte-extraktion). |
| `schema` | `object` | `null` | Ein JSON-Schema, das die zu extrahierenden Felder beschreibt. Löst die LLM-Extraktionsstufe auf Plänen aus, die sie enthalten. |
| `only_main_content` | `boolean` | `true` | Entfernt Site-Chrome (Navigation, Header, Footer, Sidebars, Cookie-Banner, versteckte Knoten) sowie Interface-Elemente (Buttons, Tab-Leisten, „War diese Seite hilfreich?“-Widgets, nur für Screenreader bestimmte Labels, Breadcrumbs) aus der `markdown`-Ausgabe und dem `main_content`-Extract, abgesichert durch einen Fidelity-Guard: Würde das Entfernen zu viel echten Inhalt streichen, wird stattdessen die vollständige Seite zurückgegeben und eine Warnung hinzugefügt. Bild-URLs werden als ihr Alt-Text gerendert (die vollständige Bildliste bleibt über `outputs: ["images"]` verfügbar). Setze `false` für die vollständige Seite, ohne dass etwas entfernt wird. Siehe [Markdown-Qualität](#markdown-qualitat). |
| `truncate_data_arrays` | `boolean` | nicht gesetzt | Kürzt lange Folgen numerischer Literale (rohe Embedding-Vektoren, Tensor-Dumps aus Notebook-Ausgabezellen) auf eine führende Stichprobe plus Anzahl, z. B. `... [truncated 1520 of 1536 values]`. Nicht gesetzt folgt `only_main_content`: aktiv, wenn die Seite aufbereitet wird, inaktiv, wenn du die Seite unverändert angefordert hast. Setze `true` oder `false`, um es explizit zu steuern. |
| `allow_degraded` | `boolean` | `false` | Gibt den Render auch dann zurück, wenn es sich um eine Anti-Bot-Challenge oder Blockierseite ohne Seiteninhalt handelt. Standardmäßig schlägt ein solcher Render mit `502` fehl, statt den Text der Zwischenseite so auszuliefern, als wäre er die Seite. |
| `direct_download` | `boolean` | `false` | Liefert die Artefakt-Bytes direkt als HTTP-Response-Body statt eines JSON-Envelopes. Erfordert genau eine Artefakt-erzeugende Ausgabe. Nur für Einzel-URL-Anfragen, denn der Batch-Endpunkt lehnt es mit `422` ab. Siehe [Direct Download](#direct-download). |
| `cache_mode` | `string` | `"enabled"` | `enabled`, `bypass` oder `refresh`. Siehe [Caching](#caching). |

### Ausgaben {: #outputs }

`outputs` akzeptiert jede Kombination dieser Namen:

| Output | Rückgabe als | Was du erhältst |
|--------|-------------|--------------|
| `markdown` | signierte URL | Sauberes Markdown der Seite. Mit `only_main_content` (Standard `true`) wird Site-Chrome wie Navigation, Header, Footer, Sidebars, Cookie-Banner und versteckte Knoten hinter einem Fidelity-Guard entfernt, und Bild-URLs werden als ihr Alt-Text gerendert. Code-Blöcke behalten in beiden Modi ihre Sprache am Fence (` ```python `). Setze `only_main_content: false` für die vollständige Seite. Siehe [Markdown-Qualität](#markdown-qualitat). |
| `html_cleaned` | signierte URL | Das gerenderte HTML, bereinigt um Skripte, Styles und Boilerplate. |
| `html_raw` | signierte URL | Das vollständige gerenderte HTML, exakt so, wie der Browser es erzeugt hat. |
| `screenshot` | signierte URL | Ein Viewport-PNG in der angeforderten (oder Standard-) Viewport-Größe. |
| `screenshot_full_page` | signierte URL | Ein Full-Page-PNG, das die gesamte Scroll-Höhe erfasst. |
| `pdf` | signierte URL | Ein PDF der Seite. Akzeptiert die vollständige `pdf_options`-Oberfläche (siehe unten). |
| `links` | signierte URL | Ein JSON-Array aller gefundenen Links, mit absoluten URLs und Ankertext. |
| `images` | signierte URL | Ein JSON-Array aller Bilder, mit absoluter `src`-URL und `alt`-Text. |
| `structured` | Inline-JSON | Strukturierte Daten, extrahiert aus der Seite (das `structured`-Antwortfeld). |

### Markdown-Qualität

Bevor die Seite konvertiert wird, wird das gerenderte DOM normalisiert,
damit das Markdown widerspiegelt, was ein Leser sieht, und nicht, wie
die Seite gebaut wurde. Das läuft bei jedem Render, sodass das Ergebnis
nicht davon abhängt, welche Extraktionsstrategie für eine Seite gewinnt.

Immer angewendet, in beiden `only_main_content`-Modi:

- **Code-Fences behalten ihre Sprache.** Die Sprache wird aus der
  jeweils vom Site verwendeten Konvention gelesen
  (`class="language-python"`, `data-lang`, ein blankes
  `language`-Attribut oder ein Highlighter-Wrapper) und normalisiert,
  sodass ` ```python ` ankommt statt eines nackten Fence.
- **Card-Links bleiben lesbar.** Ein Link, der eine Überschrift und
  eine Beschreibung umschließt, wird zu einem verlinkten Titel gefolgt
  von seiner Beschreibung, statt zu einem zusammengelaufenen Link wie
  `[DatabaseSupabase provides a full Postgres database...]`. Die
  Ziel-URL bleibt erhalten.
- **Überschriften bleiben auf einer Zeile.** Eine Überschrift, deren
  Text in einem verschachtelten Element sitzt, erzeugt kein nacktes
  `##` mehr, unter dem der Text gestrandet ist.
- **Benachbarte Elemente laufen nicht mehr zusammen.** Layouts, die
  ihre Elemente per CSS statt per Leerraum trennen, erzeugten `YesNo`
  und `EvaluationDeploymentProduction`; diese lesen sich jetzt als
  getrennte Wörter.
- **Unsichtbare Zeichen werden entfernt**: Zero-Width-Spaces als
  Anker-Labels, weiche Trennstriche und Private-Use-Area-Glyphen aus
  Icon-Fonts, die als nicht darstellbare Token ankommen.
- **Leere Elemente werden verworfen**: reine Icon-`<i>`-Elemente, die
  als verirrte `__` gerendert wurden, und Links ohne Label.

Zusätzlich mit `only_main_content: true`:

- **Interface-Steuerelemente werden entfernt**: Buttons, Tab-Leisten,
  Tastenkürzel-Hinweise, „Copy page“-/„On this page“-Aktionen und
  „War diese Seite hilfreich? Ja/Nein“-Bewertungs-Widgets. Ein
  Steuerelement mit echtem Inhalt (eine FAQ-Frage, ein klickbarer
  Card-Body) bleibt erhalten.
- **Nur für Screenreader bestimmter Text wird entfernt**: Skip-Links
  und die „Section titled ...“-Labels, die viele Doku-Themes an jede
  Überschrift hängen.
- **Von der Site deklarierter Nicht-Inhalt wird respektiert**: Blöcke
  mit `data-nosnippet`, `data-pagefind-ignore` oder `data-noindex`,
  sofern sie keine Überschriften oder Code enthalten.
- **Doppelte Blöcke werden zusammengefasst**: responsive Designs, die
  eine Desktop- und eine Mobile-Kopie derselben Leiste ausliefern, und
  Karussells, die jedes Frame vorrendern, erscheinen einmal.
- **Breadcrumbs und Eyebrow-Labels über dem Seitentitel entfallen.**

Aufgeschobener Inhalt bleibt bewusst *erhalten*: ein inaktives
Tab-Panel innerhalb der Inhaltsregion enthält ein echtes Code-Beispiel
(das Python-Beispiel in einem Tab, das JavaScript-Beispiel in einem
anderen), sodass beide ins Markdown gelangen und nicht nur der Tab, der
zum Render-Zeitpunkt zufällig ausgewählt war.

### Rendering und Warten

| Parameter | Typ | Standard | Beschreibung |
|-----------|------|---------|-------------|
| `viewport` | `object` | `1920 x 1080` | `{"width": <int>, "height": <int>}`. Breite 320–3840, Höhe 240–2160. |
| `mobile` | `boolean` | `false` | Rendert in einem mobilen Viewport (390 x 844), sofern `viewport` nicht explizit gesetzt ist. |
| `wait_for` | `string` | `null` | Wartet nach der Navigation auf einen CSS-Selektor (`".price"` oder `"css:.price"`) oder einen JS-Ausdruck (`"js:window.dataReady === true"`). |
| `wait_timeout_ms` | `integer` | `30000` | Wie lange `wait_for` warten darf, in Millisekunden. 0–60,000. Ein Timeout wird zu einer Warnung herabgestuft; die Seite wird so erfasst, wie sie ist. |
| `js_code` | `string` | `null` | JavaScript, das nach der Navigation auf der Seite ausgeführt wird. Maximal 20,000 Zeichen. Ein Fehler wird zu einer Warnung, nicht zu einem Fehlschlag. |
| `block_resources` | `string[]` | `[]` | Ressourcentypen, die vor dem Laden abgebrochen werden. Beliebige aus `image`, `media`, `font`, `stylesheet`, `script`, `xhr`, `fetch`, `websocket`, `manifest`, `other`. Nützlich für schnellere, reine Text-Renders. |
| `respect_robots` | `boolean` | `false` | Wenn `true`, wird eine von der `robots.txt` der Site untersagte URL mit `403` abgelehnt. |
| `pdf_options` | `object` | `null` | Seitenformat, Ränder, Kopf- und Fußzeilen, Skalierung und Ausrichtung für die `pdf`-Ausgabe. Dasselbe Objekt wie bei [url-to-pdf](/de/docs/endpoints/convert/web-pages/url-to-pdf.md). Ohne `pdf_options` erzeugt perceive eine einzige durchgehende Seite, byteidentisch zum V1-url-to-pdf. |

### Authentifizierte und benutzerdefinierte Anfragen

| Parameter | Typ | Standard | Beschreibung |
|-----------|------|---------|-------------|
| `auth` | `object` | `null` | HTTP Basic Auth für die Zielseite: `{"username": "...", "password": "..."}`. |
| `cookies` | `array` | `null` | Cookies, die vor der Navigation injiziert werden. Maximal 50. Jedes benötigt `name`, `value` und entweder `domain` oder `url`. |
| `headers` | `object` | `null` | Benutzerdefinierte Request-Header. Maximal 20. Blockierte Namen: `host`, `content-length`, `transfer-encoding`, `connection`, `upgrade`, `te`, `trailer`. |

<div class="alert alert-warning">
<strong>Reserviert, noch nicht live.</strong> <code>proxy_url</code> (Production+),
<code>geolocation</code> und <code>action_chain</code> werden vom
Request-Schema akzeptiert, liefern aber aktuell <code>422</code> zurück. Sie
kommen in einem späteren Release; wenn du sie jetzt sendest, erfährst du
genau, welcher Schalter noch nicht bereit ist, statt dass er still
ignoriert wird.
</div>

---

## Strukturierte Extraktion

Wenn `structured` in `outputs` enthalten ist, bestimmt die
`extract`-Liste, welche Felder perceive abruft. Forderst du nichts an,
greift der Standard `metadata` und `structured_data`.

| `extract`-Wert | Feld in `structured` | Status |
|-----------------|-----------------------|--------|
| `metadata` | `metadata` | Live |
| `structured_data` | `structured_data` (JSON-LD) | Live |
| `headings` | `headings` | Live |
| `tables` | `tables` | Live |
| `main_content` | `main_content` (Text, begrenzt auf 50,000 Zeichen) | Live |
| `all` | expandiert zu allen oben genannten Live-Feldern | Live |
| `prices` | -- | Noch nicht live: liefert eine Warnung, wird ausgelassen |
| `contacts` | -- | Noch nicht live: liefert eine Warnung, wird ausgelassen |
| `technologies` | -- | Noch nicht live: liefert eine Warnung, wird ausgelassen |

Um ehrlich zu sein: `prices`, `contacts` und `technologies` sind
reservierte Namen. Forderst du heute eines davon an, gibt es keinen
Fehler: Der Name landet im `warnings`-Array und wird aus `structured`
entfernt.

### Schema-gesteuerte Extraktion

Sende ein `schema`, um bestimmte Felder in `structured.extracted`
abzurufen:

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

Die LLM-gestützte Extraktionsstufe füllt das Schema, und
sie greift nur, wenn **alle** diese Bedingungen zutreffen: du hast ein
`schema` gesendet, dein Plan enthält die LLM-Stufe (Indie und höher),
die Seite wurde nicht als blockiert bewertet, und der heuristische
Durchlauf hat die Schemafelder leer gelassen. Läuft sie, ist
`extraction_tier` gleich `"llm"`, und `tokens` sowie `cost_cents` geben
an, was diese Extraktion gekostet hat; andernfalls ist
`extraction_tier` gleich `"heuristic"` und beide sind null.

> **Hinweis.** Die Schema-Extraktion ist hart gedeckelt, um deine
> Rechnung zu schützen: Eine einzelne Extraktion ist pro Anfrage
> begrenzt, und die Projektausgaben schöpfen aus deinem monatlichen
> AI-Credit-Guthaben ($5 / $15 / $40 pro Monat auf Indie / Studio /
> Production; ungenutzte Credits werden übertragen). LLM-Extraktion
> verbraucht Credits, keine Ops. Wird eine Obergrenze erreicht, liefert perceive das
> heuristische Ergebnis mit einem Hinweis in `warnings`, statt zu
> überziehen. Auf einem Plan ohne die LLM-Stufe erhältst du nur
> heuristische `structured`-Daten.

---

## Antwort {: #response }

Sowohl `POST /v2/perceive` als auch `GET /v2/perceive/{operation_id}`
liefern dasselbe Objekt zurück.

| Feld | Typ | Beschreibung |
|-------|------|-------------|
| `operation_id` | `string` | Opake ID (`per_...`). Verwende sie mit dem GET-Endpunkt und gib sie beim Support an. |
| `status` | `string` | `queued`, `processing`, `completed` oder `failed`. |
| `url` | `string` | Die von dir gesendete URL. |
| `url_final` | `string` | Die URL nach Weiterleitungen. |
| `content_hash` | `string` | SHA-256 der gerenderten Seite. Steuert den 1-Stunden-Cache. |
| `render_quality` | `number` | 0.0–1.0. Werte unter 0.40 bedeuten einen fehlgeschlagenen Render: eine Anti-Bot-Seite, eine Login-Wall, eine HTTP-Fehlerseite, ein Soft-404 oder eine leere Hülle. Sieh in `deductions` nach dem Grund und in `status_code` nach dem Upstream-Status. |
| `status_code` | `integer` | HTTP-Status der finalen Hauptdokument-Antwort (z. B. `200`, `404`). `null`, wenn unbekannt. |
| `deductions` | `object` | Benannte Render-Qualitätsabzüge, die gegriffen haben, z. B. `{"http_error": 0.7}`, `{"soft_404": 0.65}`, `{"login_wall": 0.65}`. Leer bei einem sauberen Render. |
| `options_echo` | `object` | Echo der Request-Optionen, die der Server berücksichtigt hat. Geheimnisse werden zu Booleans reduziert (`auth_provided`, `cookies_provided`, `headers_provided`, `js_code_provided`, `schema_provided`, `pdf_options_provided`). Die einfachen Optionen (`outputs`, `only_main_content`, `truncate_data_arrays`, `allow_degraded`, `extract`, `cache_mode`, `mobile`, `respect_robots`, `direct_download`, `wait_for`, `wait_timeout_ms`, `viewport`, `block_resources`) werden so zurückgegeben, wie sie berücksichtigt wurden. `truncate_data_arrays` wird als **aufgelöster** Boolean zurückgegeben, sodass du auch bei nicht gesetztem Wert siehst, wie entschieden wurde. |
| `cache_hit` | `boolean` | `true`, wenn das Ergebnis aus dem Cache stammt statt aus einem frischen Render. |
| `outputs` | `object` | Map von Ausgabename zu `{url, object_key, size_bytes, content_type, expires_in}`. Signierte URLs laufen nach 900 Sekunden ab. |
| `structured` | `object` | Inline-strukturierte Daten, vorhanden, wenn `structured` angefordert wurde. |
| `extraction_tier` | `string` | `heuristic`, `css` oder `llm`. |
| `tokens` | `object` | `{input, output}` verwendete LLM-Tokens. Null, sofern die LLM-Stufe nicht lief. |
| `cost_cents` | `number` | LLM-Kosten in Cent für diese Operation. Null, sofern die LLM-Stufe nicht lief. |
| `duration_ms` | `integer` | Ende-zu-Ende-Renderzeit. |
| `error` | `string` | Nur gesetzt, wenn `status` gleich `failed` ist. |
| `warnings` | `string[]` | Nicht-fatale Hinweise: ein `wait_for`-Timeout, ein übersprungenes Extraktionsfeld, eine Markierung für eine blockierte Seite, ein `only_main_content`-Fallback auf die vollständige Seite, ein Hinweis, dass lange numerische Daten-Arrays gekürzt wurden. |

---

## Eine Operation abrufen {: #retrieve-an-operation }

Signierte URLs laufen nach 15 Minuten ab. Um eine Ausgabe später
herunterzuladen, rufe die Operation erneut ab. Perceive signiert jede
URL anhand der gespeicherten Object-Keys neu. Es findet kein erneuter
Render statt, daher werden dabei keine Ops verbraucht.

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

Eine unbekannte Operation-ID oder eine, die zu einem anderen Projekt
gehört, liefert `404`. Die Existenz wird projektübergreifend nie
preisgegeben.

---

## Direct Download

Standardmäßig kommt jede Datei-Ausgabe als vorsignierte URL zurück, die
du in einer zweiten Anfrage abrufst. Setze `direct_download: true` im
POST, um den Envelope zu überspringen: Der HTTP-Response-Body **ist**
dann die Artefakt-Bytes, ohne JSON, ohne signierte URL und ohne zweiten
Abruf. Die Anfrage muss genau eine Artefakt-erzeugende Ausgabe
produzieren (`outputs: ["markdown"]`, `outputs: ["pdf"]`, …), sonst wird
sie mit `400` abgelehnt. Die Metadaten, die sonst im JSON stünden,
reisen stattdessen in Response-Headern mit: `Content-Disposition`,
`X-Operation-Id`, `X-Object-Key`, `X-Cache-Hit`, `X-Render-Quality`,
`X-Source-Status-Code`, `X-Content-Hash` und `X-Warnings-Count`.

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

Die GET-Endpunkte streamen gespeicherte Artefakte auf dieselbe Weise:

- `GET /v2/perceive/{operation_id}?direct_download=true&output=markdown`
  streamt ein Artefakt einer vergangenen Operation. `output` ist
  erforderlich, wenn die Operation mehr als ein Artefakt erzeugt hat.
  Ein Artefakt außerhalb des Aufbewahrungsfensters deines Plans
  antwortet mit `410`.
- `GET /v2/perceive/batch/{job_id}?direct_download=true` streamt die
  Batch-ZIP-Datei. Das gilt für Batches mit `output_mode: "zip"`, deren
  Archiv bereit ist, andernfalls kommt `400` zurück.

`direct_download` gilt nur für einzelne URLs: `POST /v2/perceive/batch`
lehnt es mit `422` ab. Setze `output_mode` auf `"zip"` und lade das
Archiv herunter. Siehe [Batch-Perceive](#batch-perception).

---

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

`POST /v2/perceive/batch` erfasst eine Liste von URLs, die sich einen
`options`-Block teilen. Jede URL wird über dieselbe Pipeline wie bei
einem Einzelaufruf gerendert und erzeugt ihre eigene Operationszeile.

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

Batches mit 10 oder weniger URLs laufen inline und antworten mit `200`,
wobei jedes Ergebnis befüllt ist. Größere Batches antworten mit `202`
und einer `job_id`; die URLs werden nacheinander abgearbeitet, und du
fragst die Ergebnisse ab:

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

Die Batch-Antwort meldet den aggregierten Fortschritt und enthält pro
URL ein vollständiges perceive-Ergebnis, sobald gerendert wurde:

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

`status` ist `queued`, `processing`, `completed`, `failed`, `partial`
(manche URLs erfolgreich, manche fehlgeschlagen) oder `canceled`. Setze
`output_mode` auf `zip`, um jedes Artefakt in einer einzigen ZIP-Datei
zu bündeln, die nach Abschluss des Batches im Feld `zip` zurückgegeben
wird.

### Dauerhaft und wiederaufnehmbar

Batches sind restart-sicher. Wenn der Dienst neu startet, während ein
Batch läuft, wird der Batch **automatisch fortgesetzt** und rendert nur
die URLs erneut, die noch nicht fertig waren, sodass bereits
abgeschlossene URLs ihre Artefakte behalten. Du musst einen Batch wegen
eines Neustarts nie erneut einreichen.

### Einen Batch abbrechen

`DELETE /v2/perceive/batch/{job_id}` bricht einen laufenden Batch ab.
Der Worker stoppt zwischen den URLs, sodass bereits gerenderte URLs ihre
Ergebnisse behalten und der Rest nicht gestartet wird. Der Aufruf ist
idempotent: Das Abbrechen eines bereits abgeschlossenen Batches liefert
einfach seinen aktuellen Zustand zurück, und der `status` des Batches
wird zu `canceled`.

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

---

## Caching

`cache_mode` steuert, wie perceive seinen 1-Stunden-Ergebnis-Cache
behandelt, der nach deinem Projekt, der URL und den render-relevanten
Request-Optionen geschlüsselt ist.

| `cache_mode` | Verhalten |
|--------------|-----------|
| `enabled` (Standard) | Liefert ein zwischengespeichertes Ergebnis, wenn eine identische Anfrage innerhalb der letzten Stunde gerendert wurde. `cache_hit` ist `true`, `cost_cents` ist `0`. |
| `bypass` | Überspringt den Cache und rendert neu. |
| `refresh` | Rendert neu und ersetzt den zwischengespeicherten Eintrag. |

Wichtig: Ein Cache-Hit berechnet trotzdem eine Op gegen dein
monatliches Ops-Kontingent. Das Kontingent misst Operationen, nicht
Browser-Renders, daher spart dir der Cache Renderzeit, keine Ops.

---

## Codebeispiele

### curl: Nur Markdown

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

### curl: Markdown plus strukturierte Daten

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

### curl: Alle Ausgaben plus PDF

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

### Python

```python
import requests

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

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

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

### Node.js

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

const data = await res.json();

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

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

Wenn du EnConvert aus Claude, Cursor oder einem anderen MCP-Client
aufrufst, ist dieselbe Funktion als `perceive_url`-Tool verfügbar.
Siehe [die MCP-Serverseite](/de/mcp.md).

---

## Fehlerantworten

| Status | Bedingung |
|--------|-----------|
| `400 Bad Request` | URL ist nicht `http(s)`, enthält eingebettete Zugangsdaten oder löst zu einer privaten, Loopback- oder Link-Local-Adresse auf (SSRF-Schutz). |
| `400 Bad Request` | Ungültiges `auth` (fehlendes `username`/`password`), `cookies` (kein Array, mehr als 50 Einträge, fehlende Felder) oder `headers` (kein Objekt, mehr als 20 Einträge, blockierter Name). |
| `401 Unauthorized` | Fehlender oder ungültiger API-Schlüssel / JWT-Token. |
| `402 Payment Required` | Perceive ist nicht in deinem aktuellen Plan enthalten, oder dein monatliches Ops-Kontingent ist aufgebraucht. |
| `403 Forbidden` | `/v2/perceive` ist nicht in den erlaubten Endpunkten des API-Schlüssels enthalten. |
| `403 Forbidden` | Batch ist in deinem Plan nicht verfügbar, oder die Batch-Größe überschreitet das Limit deines Plans. |
| `403 Forbidden` | `respect_robots=true`, und die `robots.txt` der Site untersagt die URL. |
| `404 Not Found` | Unbekannte `operation_id` oder `job_id`, oder eine, die einem anderen Projekt gehört. |
| `422 Unprocessable Entity` | Request-Validierung fehlgeschlagen (ungültiges Enum in `outputs`/`extract`, `wait_timeout_ms` außerhalb des Bereichs, Viewport außerhalb der Grenzen, ein unbekannter Request-Key). |
| `422 Unprocessable Entity` | `proxy_url`, `geolocation` oder `action_chain` wurde gesendet. Alle drei sind für ein späteres Release reserviert. |
| `500 Internal Server Error` | Der Render ist fehlgeschlagen. Die Meldung enthält die `operation_id`, die du beim Support angeben kannst. |
| `502 Bad Gateway` | Alle Engines wurden blockiert und der Origin lieferte eine Anti-Bot-Challenge ohne dahinterliegenden Seiteninhalt. Versuche es später erneut oder sende `allow_degraded: true`, um die Challenge-Seite unverändert zu erhalten. |

Unbekannte Request-Keys werden mit einem `422` abgelehnt, das das Feld
benennt, und zwar auf `/v2/perceive`, `/v2/perceive/batch`,
`/v2/discover` und `/v2/lookup` gleichermaßen. Still ignoriert werden
sie nie. Jeder `422`-Body enthält ein Top-Level-Array `errors` mit
menschenlesbaren Meldungen neben der rohen `detail`-Liste.

Die vollständige Statuscode-Referenz findest du im
[Fehlercode-Leitfaden](/de/docs/reference/errors.md).

---

## Limits

| Limit | Wert |
|-------|-------|
| URL-Länge | 2,048 Zeichen |
| `wait_timeout_ms` | 0–60,000 ms |
| `js_code`-Länge | 20,000 Zeichen |
| Viewport-Breite | 320–3,840 px |
| Viewport-Höhe | 240–2,160 px |
| Cookies pro Anfrage | 50 |
| Benutzerdefinierte Header pro Anfrage | 20 |
| `main_content`-Extract | 50,000 Zeichen |
| Batch-URLs pro Anfrage | 1,000 (Schema-Obergrenze) |
| Inline-Batch-Schwelle | 10 URLs (größere Batches laufen asynchron) |
| Ergebnis-Cache-TTL | 1 Stunde |
| Ablauf signierter URLs | 15 Minuten |
| Monatliche Ops (über alle Endpunkte geteilt) | 500 / 3.000 / 15.000 / 50.000 je Tarif; siehe [Preise](/de/pricing.md) |

---

## Häufig gestellte Fragen

### Wie konvertiere ich eine Webseite mit einer REST-API in Markdown?

Sende `POST /v2/perceive` mit `{"url": "...", "outputs": ["markdown"]}`. Die Seite wird in Headless-Chrome gerendert, und die Antwort enthält eine vorsignierte Download-URL für die Markdown-Datei. Standardmäßig entfernt `only_main_content` das Site-Chrome, sodass du den Artikel bekommst, nicht die Navigation; setze `"only_main_content": false` für die vollständige Seite, oder füge `"direct_download": true` hinzu, um die Markdown-Bytes direkt im Response-Body zu erhalten.

### Kann ich einen Screenshot und Markdown aus demselben Render erhalten?

Ja. `outputs` akzeptiert jede Kombination, also erzeugt `["markdown", "screenshot"]` (oder `screenshot_full_page` für die gesamte Scroll-Höhe) beides aus einem einzigen Browser-Render. Du bezahlst innerhalb eines Aufrufs nie zweimal für dieselbe Seite.

### Rendert /v2/perceive JavaScript-Seiten?

Ja. Jede Anfrage führt einen echten Headless-Chrome-Render aus: Cookie-Banner werden geschlossen, die Seite wird gescrollt, um Lazy-Loaded-Content auszulösen, und du kannst die Seite vor der Erfassung mit `wait_for` (einem CSS-Selektor oder JS-Ausdruck), `js_code` und `block_resources` steuern.

### Warum funktioniert meine signierte Download-URL nicht mehr?

Signierte URLs laufen nach 15 Minuten ab (`expires_in: 900`). Rufe die Operation mit `GET /v2/perceive/{operation_id}` erneut ab, um frisch signierte URLs zu erhalten. Es findet kein erneuter Render statt, und es werden keine Ops verbraucht.

### Zählt ein zwischengespeichertes Ergebnis trotzdem gegen mein Kontingent?

Ja. Ein Cache-Hit berechnet eine Op, denn das monatliche Kontingent misst Operationen, nicht Browser-Renders. Setze `cache_mode` auf `bypass`, um den 1-Stunden-Cache zu überspringen, oder auf `refresh`, um neu zu rendern und den zwischengespeicherten Eintrag zu ersetzen.
