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.
- Einreihen.
POSTvalidiert den Request, führt eine schnelleunits=1-Ops-Prüfung durch (Plan hat Ingest aktiviert und noch Spielraum im monatlichen Ops-Kontingent), fügt die Job-Zeile ein und antwortet mit202. Nichts wird persistiert, wenn das Ops-Gate fehlschlägt: Ein402hinterlässt null Zeilen. - Entdecken. Für den
sitemap- undcrawl-Modus führt der Worker denselben Discovery-Durchlauf aus wie der Discover-Endpunkt, begrenzt aufmax_pages, und SSRF-prüft die Seed-URL. Für denurls-Modus wird die explizite Liste der Reihe nach dedupliziert; es läuft keine Discovery. Die Discovery-Größe vor der Begrenzung wird alspages_foundgemeldet; hat die Site mehr URLs, alsmax_pageszuließ, istdiscovery_truncatedtrueund einwarnings-Eintrag nennt beide Zahlen, sodasspages_discovered(die eingereihte Anzahl) nie mit der Größe der Site verwechselt wird. - 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.
- 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. - Assemblieren. Sobald jede Seite fertig ist, werden die seitenweisen
Objekte zur finalen
v2-ingest/{job_id}.jsonlzusammengefügt, die Staging-Objekte werden gelöscht, der Job springt aufcompleted, und der signierte Abschluss-Webhook feuert, falls einewebhook_urlgesetzt 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, keinecookiesund keineheaders. 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.
POSTund das jobbezogeneGET/DELETEverwendenresponse_model_exclude_none, sodass Felder, die nochnullsind (wieoutput_urlvor dem Abschluss), aus dem JSON weggelassen statt alsnullgesendet 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.