Website für RAG crawlen – API#

POST /v2/ingest crawlt eine Website für RAG: Es verwandelt eine Site (oder eine explizite Liste von URLs) in RAG-fertige Chunks und erzeugt eine JSONL-Datei, die sich direkt in LangChain JSONLoader, LlamaIndex SimpleDirectoryReader oder einen Vektor-DB-Bulk-Import lädt. Der Endpunkt ist immer asynchron: POST antwortet mit 202 und einer job_id, du pollst GET /v2/ingest/{job_id} oder registrierst eine webhook_url, und ein abgeschlossener Job liefert eine vorsignierte output_url für die JSONL zurück. EnConvert übernimmt die Discovery, den Headless-Chrome-Render, das überschriftenbasierte Chunking und die JSONL-Assemblierung in einem Job.

Hochgeladene Dateien durchlaufen dieselbe Pipeline über POST /v2/ingest/files. PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB und mehr werden zu Markdown konvertiert, gechunkt und in dieselbe JSONL assembliert. Eine Integration deckt sowohl Web- als auch Datei-RAG-Ingestion ab.

Hier ist der kleinste nützliche Aufruf. Crawle eine Site und chunke jede Seite, die dabei gefunden wird:

curl -X POST https://api.enconvert.com/v2/ingest \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "crawl",
    "url": "https://example.com/docs"
  }'

Die Antwort ist der Job-Datensatz, zurückgegeben mit 202 Accepted. Beachte, dass der Status queued ist und output_url fehlt, bis der Job abgeschlossen ist:

{
    "job_id": "ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "status": "queued",
    "mode": "crawl",
    "pages_discovered": 0,
    "pages_processed": 0,
    "pages_failed": 0,
    "total_chunks": 0,
    "webhook_delivered": false,
    "created_at": "2026-06-24T09:14:02.118Z"
}

Ingest ist immer asynchron. Jede Seite rendert in einem echten Browser, was 10–30 Sekunden pro URL dauert, weit jenseits des 300-Sekunden-Request-Fensters für jeden nicht-trivialen Job. Also antwortet POST mit 202 und einer job_id, und ein droplet-lokaler Worker arbeitet den Job außerhalb des Requests ab. Du pollst GET /v2/ingest/{job_id} für den Fortschritt oder registrierst eine webhook_url, um benachrichtigt zu werden, wenn er fertig ist.


Endpunkte#

Methode Pfad Zweck
POST /v2/ingest Erstellt einen Web-Ingest-Job (URL-Liste, Sitemap oder Crawl). Antwortet mit 202 und einer job_id.
POST /v2/ingest/files Erstellt einen Datei-Ingest-Job aus hochgeladenen Dokumenten (Multipart). Gleiche Job- + JSONL-Pipeline.
GET /v2/ingest Liste der Jobs dieses Projekts, neueste zuerst, mit skip/limit-Paging.
GET /v2/ingest/{job_id} Lifecycle-Status eines Jobs, mit einer frisch signierten output_url, sobald abgeschlossen.
DELETE /v2/ingest/{job_id} Bricht einen Job ab. Der Worker erkennt den abgebrochenen Status und stoppt zwischen den Seiten.
POST /v2/ingest/{job_id}/retry-webhook Signiert den Abschluss-Webhook neu und POSTet ihn erneut für einen abgeschlossenen Job.
GET /v2/ingest/webhook-secret Zeigt das Webhook-Signaturgeheimnis des Projekts an (Dashboard-Kanal).
POST /v2/ingest/webhook-secret/rotate Rotiert das Signaturgeheimnis. Alte Signaturen verifizieren sofort nicht mehr.

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 nutzen die Beispiele unten.

X-API-Key: sk_your_private_key

Öffentliche Schlüssel mit einem JWT-Bearer-Token funktionieren ebenfalls, mit demselben Flow wie bei jedem anderen Endpunkt: Generiere ein Token mit deinem pk_-Schlüssel und sende es dann als Authorization: Bearer <token>. Der vollständige Flow, inklusive Domain-Locking und Token-Refresh, steht in der Authentifizierungs-Anleitung.

Jeder API-Schlüssel trägt eine Allowlist erlaubter Endpunkte. Wenn /v2/ingest nicht auf der Liste des Schlüssels steht, wird der Request mit 403 abgelehnt. Ein auf /v2/ingest beschränkter Schlüssel erreicht dennoch die von ihm erstellten Jobs: GET und DELETE /v2/ingest/{job_id} sowie POST /v2/ingest/{job_id}/retry-webhook sind für eine job_id immer erlaubt (die ing_…-Form wird explizit abgeglichen). Der statische Listen-Endpunkt und die beiden webhook-secret-Management-Routen erben diesen Bypass nicht; sie erfordern ein breiteres oder Dashboard-scoped Token.


Wie Ingest funktioniert#

Ein Job durchläuft fünf Phasen, alle dauerhaft und neustartsicher. Wenn der Worker-Prozess mitten im Job neu startet, wird der Job beim Boot erneut eingereiht und setzt bei der Seite fort, an der er gestoppt hat. Bereits abgeschlossene Seiten behalten ihre gestagte Ausgabe und werden nie erneut gerendert oder erneut berechnet.

  1. Einreihen. POST validiert den Request, führt eine schnelle units=1-Ops-Prüfung durch (Plan hat Ingest aktiviert und noch Spielraum im monatlichen Ops-Kontingent), fügt die Job-Zeile ein und antwortet mit 202. Nichts wird persistiert, wenn das Ops-Gate fehlschlägt: Ein 402 hinterlässt null Zeilen.
  2. Entdecken. Für den sitemap- und crawl-Modus führt der Worker denselben Discovery-Durchlauf aus wie der Discover-Endpunkt, begrenzt auf max_pages, und SSRF-prüft die Seed-URL. Für den urls-Modus wird die explizite Liste der Reihe nach dedupliziert; es läuft keine Discovery. Die Discovery-Größe vor der Begrenzung wird als pages_found gemeldet; hat die Site mehr URLs, als max_pages zuließ, ist discovery_truncated true und ein warnings-Eintrag nennt beide Zahlen, sodass pages_discovered (die eingereihte Anzahl) nie mit der Größe der Site verwechselt wird.
  3. Rendern und chunken. Jede URL rendert über das gemeinsame Headless-Chrome-Singleton, dieselbe Render-Pipeline hinter dem Perceive-Endpunkt. Das gerenderte HTML wird dann zu fit-Markdown konvertiert und vom überschriftenbasierten Chunker zerteilt. Renders laufen sequenziell, eine Seite nach der anderen.
  4. Zwischenspeichern. Die Chunks jeder Seite werden als seitenweises JSONL-Objekt im Storage abgelegt, mit (project, job, url) als deterministischem Schlüssel. Das macht einen Neustart günstig: Ein fortgesetzter Job verwendet zwischengespeicherte Seiten wieder, statt sie neu zu rendern.
  5. Assemblieren. Sobald jede Seite fertig ist, werden die seitenweisen Objekte zur finalen v2-ingest/{job_id}.jsonl zusammengefügt, die Staging-Objekte werden gelöscht, der Job springt auf completed, und der signierte Abschluss-Webhook feuert, falls eine webhook_url gesetzt war.

Das Ops-Kontingent wird pro Seite im Worker erneut geprüft, nicht nur beim Absenden; jede abgeschlossene Seite berechnet eine Op. Ein crawl-Job, dessen Seitenzahl vorab unbekannt ist, stoppt sauber an deinem Monatslimit: Die bereits gerenderten Seiten werden berechnet und behalten, und die verbleibenden Seiten werden als skipped markiert, statt zu viel auszugeben.

Ingest-Renders sind per Design ohne Anmeldedaten. Anders als /v2/perceive akzeptiert es kein auth, keine cookies und keine benutzerdefinierten headers. Nichts Geheimes wird für den dauerhaften Resume persistiert, sodass der Job-Status auf der Festplatte nie Anmeldedaten trägt.


Dateien ingesten#

POST /v2/ingest crawlt das Web; POST /v2/ingest/files ingested hochgeladene Dateien durch dieselbe Pipeline. Beide erstellen denselben Job, führen denselben überschriftenbasierten Chunker aus und erzeugen dasselbe einzelne JSONL-Ergebnis, sodass eine Integration Web- und Datei-RAG-Ingestion abdeckt.

Sende die Dokumente als multipart/form-data im files-Feld. Jede Datei wird vom anything-to-markdown-Konverter zu Markdown konvertiert, dann gechunkt und genau wie eine gecrawlte Seite assembliert. Jedes unterstützte Eingabeformat wird akzeptiert: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument und reiner Text/Markdown.

curl -X POST https://api.enconvert.com/v2/ingest/files \
  -H "X-API-Key: sk_your_private_key" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "max_words=700" \
  -F "webhook_url=https://your-app.example.com/hooks/ingest"

Die Antwort ist dieselbe IngestJobResponse wie beim Crawl-Endpunkt, mit mode auf files:

{
    "job_id": "ing_7c1d8e2f4a5b6c7d8e9f0a1b2c3d4e5f",
    "status": "queued",
    "mode": "files",
    "pages_discovered": 0,
    "pages_processed": 0,
    "pages_failed": 0,
    "total_chunks": 0,
    "webhook_delivered": false,
    "created_at": "2026-07-14T10:15:30.220Z"
}

Jede hochgeladene Datei zählt als eine „Seite": Sie berechnet eine Op, erhöht pages_processed, sobald sie fertig ist, und wird durch ihren Dateinamen in der JSONL-metadata.source_url gekennzeichnet. Du pollst GET /v2/ingest/{job_id}, brichst mit DELETE ab und erhältst den signierten Abschluss-Webhook genau wie bei einem Crawl-Job. Dateien werden nur gespeichert, bis die JSONL assembliert ist, dann gelöscht.

Datei-Request-Parameter#

Als Multipart-Formularfelder gesendet (kein JSON-Body):

Feld Typ Standard Beschreibung
files file[] -- Ein oder mehrere Dokumente zum Ingesten. 1–200 Dateien pro Request; jede wird gegen das Upload-Limit deines Plans größengeprüft.
max_words integer 512 Weiches Limit für Wörter pro Chunk. 32–4,000. Codeblöcke und Pipe-Tabellen bleiben atomar.
sentence_overlap integer 1 Sätze, die zwischen aufeinanderfolgenden Prosa-Chunks desselben Abschnitts wiederholt werden. 0–10.
webhook_url string null HMAC-signierter Abschluss-Callback mit derselben Signatur- und Retry-Policy wie Abschluss-Webhooks unten.

Ein nicht unterstützter Dateityp, eine leere Datei oder ein Bild (es wird kein OCR durchgeführt) wird beim Absenden mit einem 400 abgelehnt; eine Datei über dem Größenlimit pro Datei deines Plans ergibt ein 413.

import requests

BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_your_private_key"}

# Submit several files (always 202).
with open("handbook.pdf", "rb") as a, open("pricing.xlsx", "rb") as b:
    job = requests.post(
        f"{BASE}/v2/ingest/files",
        headers=HEADERS,
        files=[("files", ("handbook.pdf", a)), ("files", ("pricing.xlsx", b))],
        data={"max_words": 700},
    ).json()

# Poll GET /v2/ingest/{job_id} exactly as for a crawl job, then download output_url.
print(job["job_id"], job["status"], job["mode"])  # -> ing_...  queued  files

Request-Parameter#

Quelle und Modus#

Parameter Typ Standard Beschreibung
mode string "urls" urls, sitemap oder crawl. Legt fest, wie das URL-Set aufgebaut wird.
url string null Seed-URL für den sitemap/crawl-Modus. Muss mit http:// oder https:// beginnen. Max. 2,048 Zeichen. Für diese Modi erforderlich; im urls-Modus abgelehnt.
urls string[] null Explizite URLs zum Ingesten im urls-Modus. Nicht leer, max. 1,000 Einträge, jeweils http(s) und ≤ 2,048 Zeichen. Für den urls-Modus erforderlich; im sitemap/crawl-Modus abgelehnt.

url und urls schließen sich gegenseitig aus: Sende genau eine Quelle. Der urls-Modus erfordert urls; sitemap und crawl erfordern eine Seed-url. Das Falsche für den Modus zu senden ergibt ein 422.

Discovery (sitemap-/crawl-Modus)#

Diese werden an den Discovery-Durchlauf weitergereicht und im urls-Modus ignoriert.

Parameter Typ Standard Beschreibung
max_pages integer 50 Limit für entdeckte und ingestete URLs. 1–1,000.
max_depth integer 2 Crawl-Linktiefe ab dem Seed. 1–5.
same_domain_only boolean true Beschränkt die Discovery auf die Domain des Seeds.
include_patterns string[] [] Regex-Muster, die eine URL erfüllen muss, um behalten zu werden. Max. 50. Jedes wird beim Absenden kompiliert; ein fehlerhaftes Muster ergibt ein 422.
exclude_patterns string[] [] Regex-Muster, die eine passende URL verwerfen. Max. 50.
respect_robots boolean false Bei true wird eine durch die robots.txt der Site verbotene URL übersprungen.

Rendering#

Parameter Typ Standard Beschreibung
wait_for string null Wartet nach der Navigation auf einen CSS-Selektor oder JS-Ausdruck vor der Erfassung. Max. 1,024 Zeichen.
wait_timeout_ms integer 30000 Wie lange wait_for warten darf, in Millisekunden. 0–60,000.

Hinweis. Ingest akzeptiert kein auth, keine cookies und keine headers. Wenn eine Seite zum Rendern Anmeldedaten braucht, ist Ingest das falsche Werkzeug. Nutze für diese eine Seite den Perceive-Endpunkt, der die vollständige Oberfläche für authentifizierte Requests bietet.

Chunking (chunk-Objekt)#

Parameter Typ Standard Constraints Beschreibung
max_words integer 512 32–4,000 Weiches Limit für Wörter pro Chunk. Überschriftenbasiert. Codeblöcke und Pipe-Tabellen bleiben atomar und dürfen dies überschreiten.
sentence_overlap integer 1 0–10 Sätze, die zwischen aufeinanderfolgenden Prosa-Chunks desselben Abschnitts wiederholt werden. 0 deaktiviert das Overlap. Overlap überschreitet nie eine Überschriftengrenze.

Der Chunker teilt an #-, ##- und ###-Überschriften, sodass jeder Chunk zu genau einem Abschnitt gehört und seinen vollständigen Überschriftenpfad trägt. Tiefere Überschriften (##########) bleiben inline als Inhalt. Fenced Codeblöcke und Markdown-Tabellen werden nie geteilt, selbst wenn ein einzelner Block max_words überschreitet; Listeneinträge werden zwischen Einträgen geteilt, nie mitten im Eintrag.

Webhook#

Parameter Typ Standard Beschreibung
webhook_url string null Endpunkt, der den HMAC-signierten Abschluss-Callback empfängt. Max. 2,048 Zeichen. Schema-geprüft beim Absenden; SSRF-geprüft zum Zeitpunkt der Zustellung, nicht beim Absenden.

Response#

POST, GET /v2/ingest/{job_id} und DELETE geben alle dasselbe IngestJobResponse-Objekt zurück.

Feld Typ Beschreibung
job_id string Opake ID (ing_…). Verwende sie mit den GET/DELETE-Endpunkten und nenne sie dem Support.
status string queued, discovering, processing, completed, failed oder canceled.
mode string Der von dir übermittelte Modus: urls, sitemap, crawl oder files.
pages_discovered integer Elemente, die der Job tatsächlich eingereiht hat: URLs (die explizite Liste oder das auf max_pages begrenzte Discovery-Ergebnis) oder hochgeladene Dateien. pages_processed + pages_failed summieren sich darauf, sobald der Job terminal ist.
pages_found integer Eindeutige zulässige URLs, die die Discovery vor der max_pages-Begrenzung ergab. Für sitemap-Jobs ist dies die tatsächliche eindeutige Anzahl der Site; für crawl-Jobs ist es eine Untergrenze (der Crawl stoppt das Abrufen an der Begrenzung). Fehlt bei urls- und files-Jobs.
discovery_truncated boolean true, wenn die Discovery mehr eindeutige URLs fand, als max_pages den Job einreihen ließ. Ein warnings-Eintrag nennt die Zahlen; erhöhe max_pages, um mehr von der Site zu ingesten.
pages_processed integer URLs, deren render → chunk → stage abgeschlossen wurde.
pages_failed integer URLs, die nicht gerendert werden konnten oder übersprungen wurden (z. B. Ops-Kontingent erschöpft).
total_chunks integer Insgesamt geschriebene Chunks über alle abgeschlossenen Seiten. Entspricht der JSONL-Zeilenanzahl.
output_url string Vorsignierte Download-URL für die finale JSONL. Nur vorhanden, sobald status completed ist; läuft nach 15 Minuten ab.
error_message string Gesetzt, wenn status failed ist (z. B. Discovery abgelehnt, alle Seiten fehlgeschlagen).
webhook_url string Das für diesen Job registrierte Abschluss-Webhook-Ziel, falls vorhanden.
webhook_delivered boolean true, sobald der signierte Abschluss-Webhook ein 2xx erhalten hat.
created_at string Wann der Job erstellt wurde (UTC).
completed_at string Wann der Job einen terminalen Status erreichte (UTC).
warnings string[] Nicht fatale Hinweise, z. B. Discovery-Trunkierung: "discovery found 719 unique URLs; the job was capped at max_pages=50, so 50 pages were enqueued. Raise max_pages to ingest more of the site."

Hinweis. POST und das jobbezogene GET/DELETE verwenden response_model_exclude_none, sodass Felder, die noch null sind (wie output_url vor dem Abschluss), aus dem JSON weggelassen statt als null gesendet werden.

Die Form des JSONL-Datensatzes#

Die finale Datei ist zeilengetrenntes JSON. Jede Zeile ist ein Chunk:

{"id":"9f2b8c1ad4e5-0000","content":"Pricing is usage-based...","metadata":{"source_url":"https://example.com/pricing","title":"Pricing","headings_path":["Pricing","Plans"],"section":"Plans","word_count":118,"chunk_index":0}}
Feld Typ Beschreibung
id string Deterministisch pro (source_url, chunk_index): <md5(url)[:12]>-<index:04d>. Ein erneuter Lauf erzeugt identische IDs.
content string Der abrufbare Chunk-Text. Wird in LangChain auf Document.page_content gemappt.
metadata.source_url string Die Seite, aus der der Chunk stammt.
metadata.title string <title> der Seite, ersatzweise das erste <h1>, begrenzt auf 512 Zeichen.
metadata.headings_path string[] Der h1 → h2 → h3-Pfad, unter dem der Chunk liegt.
metadata.section string Der innerste Überschriftentext (der letzte Eintrag von headings_path).
metadata.word_count integer Durch Whitespace getrennte Wortanzahl von content.
metadata.chunk_index integer Der Index des Chunks innerhalb seiner Seite.

Die Datei ist UTF-8, geschrieben mit ensure_ascii=false, sodass Unicode lesbar bleibt. Weil content ein String auf oberster Ebene und metadata ein Geschwister-Objekt ist, lädt dieselbe Datei über LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader und jeden zeilenorientierten Vektor-DB-Import ohne Umformen.


Job-Lifecycle und Polling#

Ein Job durchläuft diese Zustände:

queued → discovering → processing → completed | failed | canceled
Status Bedeutung
queued Angenommen und wartet auf den Worker.
discovering Führt den sitemap-/crawl-Discovery-Durchlauf aus (nur sitemap/crawl).
processing Rendert und chunkt Seiten. pages_processed und total_chunks steigen live.
completed Die finale JSONL ist assembliert; output_url ist signiert und bereit.
failed Discovery wurde abgelehnt, oder jede Seite ist fehlgeschlagen oder wurde übersprungen. error_message erklärt es.
canceled Ein DELETE erreichte den Job, bevor er fertig war.

Polle den Status mit dem jobbezogenen GET. Das ist read-only: Es verbraucht keine Ops und signiert die output_url bei jedem Aufruf neu aus dem gespeicherten Objektschlüssel:

curl https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key"

Eine unbekannte job_id oder eine, die zu einem anderen Projekt gehört, gibt 404 zurück. Die Existenz wird nie projektübergreifend preisgegeben.

Jobs auflisten#

GET /v2/ingest gibt die Jobs dieses Projekts neueste zuerst zurück, mit den Query-Parametern skip und limit. limit ist standardmäßig 20 und auf 100 begrenzt. Die Antwort trägt ein has_more-Flag statt einer Gesamtanzahl:

curl "https://api.enconvert.com/v2/ingest?skip=0&limit=20" \
  -H "X-API-Key: sk_your_private_key"
{
    "jobs": [
        {
            "job_id": "ing_3f9a...",
            "status": "completed",
            "mode": "crawl",
            "pages_discovered": 42,
            "pages_found": 42,
            "discovery_truncated": false,
            "pages_processed": 41,
            "pages_failed": 1,
            "total_chunks": 1187,
            "output_url": "https://spaces.example.com/...signed...",
            "webhook_configured": true,
            "webhook_delivered": true,
            "created_at": "2026-06-24T09:14:02.118Z",
            "completed_at": "2026-06-24T09:31:55.402Z"
        }
    ],
    "skip": 0,
    "limit": 20,
    "has_more": false
}

Die Listenzeilen reduzieren webhook_url auf einen webhook_configured-Boolean, sodass die Liste den rohen Endpunkt nie in die Tabelle zurückgibt.

Einen Job abbrechen#

DELETE /v2/ingest/{job_id} setzt den Status des Jobs auf canceled. Der Worker liest diesen Status zwischen den Seiten und stoppt, ohne die Ausgabe zu assemblieren. Der Abbruch ist idempotent und race-sicher: Wenn die Assemblierung bereits committet wurde, trifft das DELETE nichts und der Job wird unverändert als completed zurückgegeben. Ein fertiger Job wird nie auf canceled zurückgesetzt.

curl -X DELETE \
  https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key"

Abschluss-Webhooks#

Setze webhook_url beim POST, und EnConvert sendet einen HMAC-signierten POST, wenn der Job abgeschlossen ist. Die Payload ist kompaktes, nach Schlüsseln sortiertes JSON:

{"job_id":"ing_3f9a...","output_url":"https://spaces.example.com/...signed...","pages_processed":41,"status":"completed","total_chunks":1187}

Die Zustellung wird nach dem ersten Versuch bis zu dreimal wiederholt, mit Back-off-Verzögerungen von 1, 4 und 16 Sekunden, also vier POSTs im schlimmsten Fall. Jeder Versuch wird mit einem frischen Timestamp neu signiert, sodass eine langsame Retry-Kette nie über das Frische-Fenster des Consumers hinausdriftet. Eine 2xx-Antwort ist ein Erfolg. Ein toter Endpunkt wird als Nicht-Zustellung erfasst und löst einen Dashboard-Alert aus, lässt einen ansonsten abgeschlossenen Job aber nie scheitern.

Die webhook_url wird zum Zeitpunkt der Zustellung SSRF-geprüft, nicht beim Absenden. Eine URL, die zu einer privaten, Loopback- oder Metadaten-Adresse auflöst, wird inert gespeichert und erst abgelehnt, wenn EnConvert versucht, dorthin zu POSTen.

Die Signatur verifizieren#

Jede Zustellung trägt zwei Header:

Header Wert
X-Enconvert-Signature sha256=<hex>, das HMAC-SHA256 von <timestamp>.<raw body>.
X-Enconvert-Timestamp Der in die Signatur eingebundene Unix-Sekunden-Timestamp.

Die Signatureingabe ist der Timestamp, ein literaler ., dann der rohe Request-Body. Das Einbinden des Timestamps in den MAC bedeutet, dass ein Consumer, der veraltete Timestamps ablehnt, Replay-Schutz gratis bekommt. Das Standard-Frische-Fenster beträgt 300 Sekunden. Verifiziere in deinem Handler:

import hashlib
import hmac
import time

SECRET = "whsec_your_signing_secret"   # from GET /v2/ingest/webhook-secret
TOLERANCE_SECONDS = 300


def verify(raw_body: bytes, signature_header: str, timestamp_header: str) -> bool:
    if not signature_header or not timestamp_header:
        return False
    try:
        ts = int(timestamp_header)
    except ValueError:
        return False
    if abs(time.time() - ts) > TOLERANCE_SECONDS:
        return False  # replayed or badly skewed clock

    provided = signature_header.removeprefix("sha256=")
    expected = hmac.new(
        SECRET.encode("utf-8"),
        f"{ts}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, provided)

Das Signaturgeheimnis verwalten#

GET /v2/ingest/webhook-secret zeigt das Geheimnis des Projekts an (erstellt es beim ersten Aufruf) zusammen mit den Header-Namen und der Toleranz, die dein Consumer braucht. Es ist sensibel und wird nur über den authentifizierten Dashboard-Kanal offengelegt:

{
    "secret": "whsec_...",
    "signature_header": "X-Enconvert-Signature",
    "timestamp_header": "X-Enconvert-Timestamp",
    "signature_scheme": "sha256",
    "replay_tolerance_seconds": 300,
    "rotated": false
}

POST /v2/ingest/webhook-secret/rotate stellt ein neues Geheimnis aus und setzt rotated auf true. Jede mit dem vorherigen Geheimnis berechnete Signatur verifiziert in dem Moment nicht mehr, in dem die Rotation committet. Rotiere nach einem vermuteten Leak und aktualisiere dann deinen Consumer.

Einen Webhook erneut zustellen#

Wenn dein Endpunkt beim Abschluss des Jobs ausgefallen war, signiert POST /v2/ingest/{job_id}/retry-webhook neu und POSTet erneut mit derselben Retry-Policy:

curl -X POST \
  https://api.enconvert.com/v2/ingest/ing_3f9a.../retry-webhook \
  -H "X-API-Key: sk_your_private_key"
{
    "job_id": "ing_3f9a...",
    "delivered": true,
    "attempts": 1,
    "status_code": 200,
    "detail": "Delivered (HTTP 200)."
}

Es gibt 404 für eine unbekannte oder fremde job_id zurück, 400, wenn keine webhook_url konfiguriert ist (oder die gespeicherte URL nun zu einer privaten/internen Adresse auflöst), und 409, wenn der Job completed nicht erreicht hat.


Codebeispiele#

curl: explizite URL-Liste#

curl -X POST https://api.enconvert.com/v2/ingest \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "urls",
    "urls": [
      "https://example.com/docs/intro",
      "https://example.com/docs/quickstart",
      "https://example.com/docs/api"
    ]
  }'

curl: Crawl mit Chunking und einem Webhook#

curl -X POST https://api.enconvert.com/v2/ingest \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "crawl",
    "url": "https://example.com/docs",
    "max_pages": 200,
    "max_depth": 3,
    "include_patterns": ["/docs/"],
    "chunk": {"max_words": 700, "sentence_overlap": 2},
    "webhook_url": "https://your-app.example.com/hooks/ingest"
  }'

Python: absenden, pollen, herunterladen#

import time

import requests

BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_your_private_key"}

# 1. Submit (always 202).
job = requests.post(
    f"{BASE}/v2/ingest",
    headers=HEADERS,
    json={"mode": "crawl", "url": "https://example.com/docs", "max_pages": 100},
).json()
job_id = job["job_id"]

# 2. Poll until terminal.
while True:
    job = requests.get(f"{BASE}/v2/ingest/{job_id}", headers=HEADERS).json()
    if job["status"] in ("completed", "failed", "canceled"):
        break
    time.sleep(5)

# 3. Download the JSONL from its signed URL.
if job["status"] == "completed":
    jsonl = requests.get(job["output_url"]).text
    print(f"{job['total_chunks']} chunks across "
          f"{job['pages_processed']} pages")
    print(jsonl.splitlines()[0])

Node.js: absenden und pollen#

const BASE = "https://api.enconvert.com";
const HEADERS = {
    "Content-Type": "application/json",
    "X-API-Key": "sk_your_private_key"
};

// 1. Submit.
const submit = await fetch(`${BASE}/v2/ingest`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
        mode: "crawl",
        url: "https://example.com/docs",
        max_pages: 100
    })
});
let job = await submit.json();

// 2. Poll until terminal.
while (!["completed", "failed", "canceled"].includes(job.status)) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await fetch(`${BASE}/v2/ingest/${job.job_id}`, {
        headers: { "X-API-Key": HEADERS["X-API-Key"] }
    });
    job = await poll.json();
}

// 3. Download the JSONL.
if (job.status === "completed") {
    const jsonl = await fetch(job.output_url).then((r) => r.text());
    console.log(`${job.total_chunks} chunks`);
    console.log(jsonl.split("\n")[0]);
}

Fehlerantworten#

Status Bedingung
202 Accepted Der Job wurde erstellt und eingereiht. Das ist das normale POST-Ergebnis.
401 Unauthorized Fehlender oder ungültiger API-Schlüssel / JWT-Token.
402 Payment Required Ingest ist nicht in deinem aktuellen Plan enthalten, oder dein monatliches Ops-Kontingent ist erschöpft.
403 Forbidden /v2/ingest ist nicht in den erlaubten Endpunkten des API-Schlüssels.
404 Not Found Unbekannte job_id oder eine, die einem anderen Projekt gehört.
409 Conflict retry-webhook auf einem Job aufgerufen, der completed nicht erreicht hat.
400 Bad Request retry-webhook aufgerufen, ohne dass eine webhook_url konfiguriert ist, oder ihre gespeicherte URL löst nun zu einer privaten/internen Adresse auf.
422 Unprocessable Entity Quelle passt nicht zum Modus (urls ohne urls oder eine Seed-url im urls-Modus); ein Parameter ist außerhalb des Bereichs; oder ein include_patterns/exclude_patterns-Regex kompiliert nicht.
500 Internal Server Error Der Job konnte nicht erstellt werden. Die Nachricht enthält die job_id, die du dem Support nennst.

Ein Render-Fehler auf Seitenebene lässt weder den Request noch den Job scheitern. Er erhöht pages_failed, legt den Fehler der Seite in ihrer eigenen Zeile ab, und der Job läuft weiter. Ein Job fails nur, wenn die Discovery abgelehnt wird oder jede Seite fehlschlägt oder übersprungen wird. Die vollständige Statuscode-Referenz steht in der Fehlercode-Anleitung.


Limits#

Limit Wert
URLs pro Request im urls-Modus 1,000
Länge von url / jedem urls-Eintrag 2,048 characters
max_pages (Discovery-Limit) 1–1,000
max_depth 1–5
include_patterns / exclude_patterns 50 each
Länge von wait_for 1,024 characters
wait_timeout_ms 0–60,000 ms
chunk.max_words 32–4,000 (default 512)
chunk.sentence_overlap 0–10 (default 1)
Länge von webhook_url 2,048 characters
Seitenobergrenze pro Job (MAX_PAGES_PER_JOB) 1,000
Dateien pro /v2/ingest/files-Request 1–200
Upload-Größe pro Datei Planabhängig (Founding: 5 MB)
GET /v2/ingest-Listen-limit 1–100 (default 20)
Ablauf der signierten output_url 15 Minuten
Webhook-Zustellversuche 4 (initial + 3 Retries)
Webhook-Replay-Toleranz 300 Sekunden
Monatliche Ops (über alle Endpunkte geteilt, 1 pro Seite) 500 / 3.000 / 15.000 / 50.000 je Tarif; siehe Preise

Häufig gestellte Fragen#

Wie crawle ich eine Website für RAG mit einer API?#

Sende POST /v2/ingest mit mode: "crawl" und einer Seed-url. Der Aufruf antwortet mit 202 und einer job_id; der Worker entdeckt Seiten, rendert jede in Headless Chrome, chunkt das Markdown überschriftenbasiert und assembliert eine JSONL-Datei, die du von der signierten output_url herunterlädst.

Wie ingeste ich Dateien (PDFs, Word-Dokumente) für RAG?#

Sende POST /v2/ingest/files als multipart/form-data mit einer oder mehreren files. Jedes Dokument wird zu Markdown konvertiert, überschriftenbasiert gechunkt und in dieselbe einzelne JSONL wie ein Crawl-Job assembliert, sodass eine Pipeline Web und Dateien abdeckt. PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument und Dateien in reinem Text/Markdown werden unterstützt (bis zu 200 pro Request); die vollständige Liste steht auf der anything-to-markdown-Seite.

Lädt die JSONL-Ausgabe direkt in LangChain und LlamaIndex?#

Ja. Jede Zeile trägt einen content-String auf oberster Ebene mit einem Geschwister-Objekt metadata, sodass dieselbe Datei über LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader und jeden zeilenorientierten Vektor-DB-Import ohne Umformen lädt.

Wie teilt der Chunker Seiten in RAG-Chunks auf?#

Er teilt an #-, ##- und ###-Überschriften mit einem weichen max_words-Limit (Standard 512, Bereich 32–4,000) und optionalem sentence_overlap. Fenced Codeblöcke und Markdown-Tabellen werden nie geteilt, und jeder Chunk trägt seinen vollständigen headings_path.

Wie werde ich benachrichtigt, wenn ein Ingest-Job fertig ist?#

Setze webhook_url beim POST, und EnConvert sendet einen HMAC-signierten Callback (Header X-Enconvert-Signature und X-Enconvert-Timestamp) mit bis zu drei Retries nach dem ersten Versuch. Wenn dein Endpunkt ausgefallen war, signiert POST /v2/ingest/{job_id}/retry-webhook neu und stellt ihn erneut zu.

Warum fehlt output_url in meiner Ingest-Antwort?#

output_url ist nur vorhanden, sobald status completed ist. Die POST-Antwort ist ein queued-Job, bei dem das Feld weggelassen wird. Polle GET /v2/ingest/{job_id}, was keine Ops verbraucht und die URL bei jedem Aufruf neu signiert; jede signierte URL läuft nach 15 Minuten ab.