Ingest
POST /v2/ingest verwandelt eine Website — oder eine explizite Liste von URLs — in
RAG-fertige Chunks und erzeugt eine einzige JSONL-Datei, die sich direkt in
LangChain JSONLoader, LlamaIndex SimpleDirectoryReader oder einen Vektor-DB-Bulk-Import
laden lässt. Sie ersetzt den zweistufigen Stack, den du sonst bauen müsstest:
Firecrawl /crawl zum Entdecken und Scrapen, plus deinen eigenen Chunker, um das
Markdown zu zerlegen. EnConvert erledigt die Entdeckung, das Headless-Chrome-Rendering,
das überschriftenbewusste Chunking und das Zusammensetzen der JSONL in einem Job.
Hier ist der kleinste sinnvolle Aufruf. Crawle eine Website und chunke jede Seite, die sie entdeckt:
curl -X POST https://api.enconvert.com/v2/ingest \
-H "X-API-Key: sk_live_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 wird in einem echten Browser gerendert,
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 Request-Pfads 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.
Gleich vorab erwähnenswert.
/v2/ingestist ein V2-Endpunkt mit seinem eigenen Quota-Zähler,ingest_pages. V1-Konvertierungspläne enthalten keine Ingest-Freimenge — du brauchst einen Plan mit V2-Inklusivleistungen, um ihn aufzurufen. Eine Seite wird für jede URL abgerechnet, deren Render-, Chunk- und JSONL-Stufe abschließt; Seiten, die fehlschlagen oder übersprungen werden, werden nicht abgerechnet. Siehe die Preis- seite für die Freimengen pro Tarif und die V2-Übersicht dazu, wie sich die V2-Zähler von V1-Konvertierungen unterscheiden.
Endpunkte
| Methode | Pfad | Zweck |
|---|---|---|
POST |
/v2/ingest |
Erstellt einen Ingest-Job. Antwortet mit 202 und einer job_id. |
GET |
/v2/ingest |
Neueste-zuerst-Liste der Jobs dieses Projekts, mit skip/limit-Paging. |
GET |
/v2/ingest/{job_id} |
Lebenszyklus-Status eines Jobs, mit einer frisch signierten output_url, sobald abgeschlossen. |
DELETE |
/v2/ingest/{job_id} |
Bricht einen Job ab. Der Worker sieht den abgebrochenen Status und stoppt zwischen Seiten. |
POST |
/v2/ingest/{job_id}/retry-webhook |
Signiert und sendet den Abschluss-Webhook für einen abgeschlossenen Job erneut per POST. |
GET |
/v2/ingest/webhook-secret |
Zeigt das Webhook-Signing-Secret des Projekts (Dashboard-Kanal). |
POST |
/v2/ingest/webhook-secret/rotate |
Rotiert das Signing-Secret. 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. Dies ist der Weg, den die folgenden Beispiele nutzen.
X-API-Key: sk_live_your_private_key
Öffentliche Schlüssel mit einem JWT-Bearer-Token funktionieren ebenfalls und nutzen denselben Ablauf wie
jeder andere Endpunkt — erzeuge ein Token mit deinem pk_-Schlüssel und sende es dann
als Authorization: Bearer <token>. Der vollständige Ablauf, einschließlich Domain-
Locking und Token-Refresh, steht in der Authentifizierungsanleitung.
Jeder API-Schlüssel trägt eine Allowlist erlaubter Endpunkte. Wenn /v2/ingest nicht
auf der Liste des Schlüssels steht, wird die Anfrage mit 403 abgelehnt. Ein Schlüssel, der
auf /v2/ingest eingegrenzt ist, erreicht trotzdem 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 gematcht).
Der statische Listen-Endpunkt und die beiden webhook-secret-Verwaltungsrouten
erben diesen Bypass nicht; sie erfordern ein breiteres oder dashboard-eingegrenztes
Token.
So funktioniert Ingest
Ein Job durchläuft fünf Phasen, alle dauerhaft und neustartsicher. Wenn der Worker-Prozess mitten im Job neu startet, wird der Job beim Booten erneut eingereiht und nimmt bei der Seite wieder auf, bei der er stoppte — bereits abgeschlossene Seiten behalten ihre gestagete Ausgabe und werden nie erneut gerendert oder erneut abgerechnet.
- Queue.
POSTvalidiert die Anfrage, führt eine schnelleunits=1-Quota- Prüfung durch (Plan hat Ingest aktiviert und etwas monatlichen Spielraum), fügt den Job-Datensatz ein und antwortet mit202. Es wird nichts persistiert, wenn das Quota-Gate fehlschlägt — ein402hinterlässt null Datensätze. - Discover. Für
sitemap- undcrawl-Modus führt der Worker denselben Entdeckungsdurchlauf wie der Discover-Endpunkt aus, gedeckelt aufmax_pages, und prüft die Seed-URL per SSRF-Screening. Fürurls-Modus wird die explizite Liste in Reihenfolge dedupliziert; es läuft keine Entdeckung. - Render und Chunk. Jede URL wird über das gemeinsame Headless- Chrome-Singleton gerendert — dieselbe Render-Pipeline hinter dem Perceive-Endpunkt — dann wird das gerenderte HTML in fit-Markdown konvertiert und vom überschriftenbewussten Chunker zerlegt. Renderings laufen sequenziell, eine Seite nach der anderen.
- Stage. Die Chunks jeder Seite werden in ein JSONL-Objekt pro Seite im
Speicher geschrieben, deterministisch verschlüsselt nach
(project, job, url). Genau das macht einen Neustart günstig: Ein wiederaufgenommener Job nutzt gestagete Seiten erneut, statt sie neu zu rendern. - Assemble. Sobald jede Seite fertig ist, werden die Objekte pro Seite in
die finale
v2-ingest/{job_id}.jsonlaneinandergehängt, die Staging- Objekte werden gelöscht, der Job wechselt zucompletedund — falls einewebhook_urlgesetzt war — feuert der signierte Abschluss-Webhook.
Der Quota-Zähler wird pro Seite im Worker erneut geprüft, nicht nur
zum Einreichungszeitpunkt. Ein crawl-Job, dessen Seitenanzahl im Voraus unbekannt ist, stoppt
sauber an deiner monatlichen Obergrenze: Die bereits gerenderten Seiten werden abgerechnet und
behalten, und die verbleibenden Seiten werden als skipped markiert, statt
zu überziehen.
Ingest-Renderings sind prinzipbedingt credential-frei. Anders als /v2/perceive
akzeptiert es kein auth, keine cookies und keine eigenen headers — nichts Geheimes
wird für den dauerhaften Resume persistiert, sodass der Job-Status auf der Platte nie
Credentials trägt.
Request-Parameter
Quelle und Modus
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
mode |
string |
"urls" |
urls, sitemap oder crawl. Wählt, wie das URL-Set aufgebaut wird. |
url |
string |
null |
Seed-URL für sitemap/crawl-Modus. Muss mit http:// oder https:// beginnen. Max. 2.048 Zeichen. Erforderlich für diese Modi; in urls-Modus abgelehnt. |
urls |
string[] |
null |
Explizite URLs, die im urls-Modus ingestiert werden. Nicht leer, max. 1.000 Einträge, jede http(s) und ≤ 2.048 Zeichen. Erforderlich für urls-Modus; in sitemap/crawl-Modus abgelehnt. |
url und urls schließen sich gegenseitig aus — genau eine Quelle. urls-Modus
erfordert urls; sitemap und crawl erfordern eine Seed-url. Den
falschen für den Modus zu senden, ist ein 422.
Discovery (sitemap/crawl-Modi)
Diese werden an den Entdeckungsdurchlauf weitergeleitet und im urls-Modus ignoriert.
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
max_pages |
integer |
50 |
Obergrenze für entdeckte und ingestierte URLs. 1–1.000. |
max_depth |
integer |
2 |
Crawl-Linktiefe ab dem Seed. 1–5. |
same_domain_only |
boolean |
true |
Beschränkt die Entdeckung 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 Einreichen kompiliert; ein fehlerhaftes Muster ist ein 422. |
exclude_patterns |
string[] |
[] |
Regex-Muster, die eine passende URL verwerfen. Max. 50. |
respect_robots |
boolean |
false |
Wenn true, wird eine durch die robots.txt der Website verbotene URL übersprungen. |
Rendering
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
wait_for |
string |
null |
Wartet nach der Navigation auf einen CSS-Selektor oder JS-Ausdruck, bevor erfasst wird. 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 Credentials zum Rendern braucht, ist Ingest das falsche Werkzeug — nutze den Perceive-Endpunkt, der die volle authentifizierte Request-Oberfläche trägt, für diese eine Seite.
Chunking (chunk-Objekt)
| Parameter | Typ | Standard | Einschränkungen | Beschreibung |
|---|---|---|---|---|
max_words |
integer |
512 |
32–4.000 | Weiche Obergrenze für Wörter pro Chunk. Überschriftenbewusst. Code-Blö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 den Overlap. Der Overlap überschreitet nie eine Überschriftengrenze. |
Der Chunker teilt an #-, ##- und ###-Überschriften — jeder Chunk gehört
zu genau einem Abschnitt und trägt seinen vollständigen Überschriftenpfad. Tiefere
Überschriften (####–######) bleiben inline als Inhalt. Eingezäunte Code-Blöcke und
Markdown-Tabellen werden nie geteilt, selbst wenn ein einzelner Block
max_words überschreitet; Listeneinträge teilen sich zwischen Einträgen, 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 wird beim Einreichen geprüft; SSRF-Screening erfolgt zum Zustellzeitpunkt, nicht beim Einreichen. |
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_…). Nutze 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 eingereichte Modus: urls, sitemap oder crawl. |
pages_discovered |
integer |
URLs, die der Job ingestieren wird (die explizite Liste oder das Entdeckungsergebnis). |
pages_processed |
integer |
URLs, deren Render → Chunk → Stage abgeschlossen wurde. |
pages_failed |
integer |
URLs, die beim Rendern fehlschlugen oder übersprungen wurden (z. B. Quota 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; verfällt nach 15 Minuten. |
error_message |
string |
Gesetzt, wenn status failed ist (z. B. Entdeckung 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 erhielt. |
created_at |
string |
Wann der Job erstellt wurde (UTC). |
completed_at |
string |
Wann der Job einen Endstatus erreichte (UTC). |
warnings |
string[] |
Nicht-fatale Hinweise. |
Hinweis.
POSTund dasGET/DELETEpro Job nutzenresponse_model_exclude_none, sodass Felder, die nochnullsind (wieoutput_urlvor dem Abschluss), aus dem JSON ausgelassen 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. Bildet auf Document.page_content in LangChain ab. |
metadata.source_url |
string |
Die Seite, von der der Chunk stammt. |
metadata.title |
string |
Seiten-<title>, mit Rückfall auf das erste <h1>, gedeckelt auf 512 Zeichen. |
metadata.headings_path |
string[] |
Der h1 → h2 → h3-Pfad, unter dem der Chunk sitzt. |
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. Da content ein String auf oberster Ebene und metadata ein
Geschwister-Objekt ist, lädt dieselbe Datei durch LangChain
JSONLoader(content_key="content", json_lines=True), LlamaIndex
SimpleDirectoryReader und jeden zeilenorientierten Vektor-DB-Import ohne
Umformen.
Job-Lebenszyklus 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-Entdeckungsdurchlauf aus (nur sitemap/crawl). |
processing |
Rendert und chunkt Seiten. pages_processed und total_chunks steigen live. |
completed |
Die finale JSONL ist zusammengesetzt; output_url ist signiert und bereit. |
failed |
Die Entdeckung wurde abgelehnt, oder jede Seite schlug fehl oder wurde übersprungen. error_message erklärt es. |
canceled |
Ein DELETE erreichte den Job, bevor er fertig war. |
Polle den Status mit dem GET pro Job. Das ist nur lesend — es verbraucht
keine Quota und signiert die output_url bei jedem Aufruf aus dem gespeicherten Objekt-Schlüssel
neu:
curl https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
-H "X-API-Key: sk_live_your_private_key"
Eine unbekannte job_id oder eine, die einem anderen Projekt gehört, gibt
404 zurück — Existenz wird nie über Projekte hinweg preisgegeben.
Jobs auflisten
GET /v2/ingest gibt die Jobs dieses Projekts neueste-zuerst zurück, mit skip- und
limit-Query-Parametern. limit ist standardmäßig 20 und auf 100 gedeckelt. 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_live_your_private_key"
{
"jobs": [
{
"job_id": "ing_3f9a...",
"status": "completed",
"mode": "crawl",
"pages_discovered": 42,
"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 — die
Liste gibt den rohen Endpunkt nie in die Tabelle zurück.
Einen Job abbrechen
DELETE /v2/ingest/{job_id} setzt den Status des Jobs auf canceled. Der
Worker liest diesen Status zwischen Seiten und stoppt, ohne Ausgabe zusammenzusetzen.
Der Abbruch ist idempotent und race-sicher: Wenn das Zusammensetzen bereits
committed ist, matcht 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_live_your_private_key"
Abschluss-Webhooks
Setze webhook_url beim POST, und EnConvert sendet einen HMAC-signierten POST,
wenn der Job abschließt. 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 versucht es nach dem ersten Versuch bis zu dreimal erneut, mit Back-off- Verzögerungen von 1, 4 und 16 Sekunden — vier POSTs im schlimmsten Fall. Jeder Versuch wird mit einem frischen Zeitstempel neu signiert, sodass eine langsame Retry-Kette nie über das Frische-Fenster des Konsumenten hinaus driftet. Eine 2xx-Antwort ist ein Erfolg; ein toter Endpunkt wird als Nicht-Zustellung erfasst (und löst eine Dashboard-Warnung aus), er versenkt einen ansonsten abgeschlossenen Job nie.
Die webhook_url wird zum Zustellzeitpunkt per SSRF gescreent, nicht beim Einreichen. Eine
URL, die auf eine private, Loopback- oder Metadaten-Adresse auflöst, wird inert
gespeichert und erst abgelehnt, wenn EnConvert versucht, an sie zu POSTen.
Die Signatur verifizieren
Jede Zustellung trägt zwei Header:
| Header | Wert |
|---|---|
X-Enconvert-Signature |
sha256=<hex> — der HMAC-SHA256 von <timestamp>.<raw body>. |
X-Enconvert-Timestamp |
Der Unix-Sekunden-Zeitstempel, der in die Signatur gebunden ist. |
Der Signing-Input ist der Zeitstempel, ein literales ., dann der rohe Request-
Body. Den Zeitstempel in den MAC zu binden bedeutet, dass ein Konsument, der
veraltete Zeitstempel ablehnt, Replay-Schutz gratis erhält — das Standard-Frische-
Fenster ist 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 Signing-Secret verwalten
GET /v2/ingest/webhook-secret zeigt das Secret des Projekts (und erstellt es
beim ersten Aufruf) zusammen mit den Header-Namen und der Toleranz, die dein Konsument
braucht. Es ist sensibel und wird nur über den authentifizierten
Dashboard-Kanal preisgegeben:
{
"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 gibt ein neues Secret aus und setzt
rotated auf true. Jede mit dem vorherigen Secret berechnete Signatur
verifiziert in dem Moment nicht mehr, in dem die Rotation committet — rotiere nach einem vermuteten
Leak und aktualisiere dann deinen Konsumenten.
Einen Webhook erneut zustellen
Wenn dein Endpunkt ausgefallen war, als der Job fertig wurde,
signiert und sendet POST /v2/ingest/{job_id}/retry-webhook mit
derselben Retry-Policy erneut per POST:
curl -X POST \
https://api.enconvert.com/v2/ingest/ing_3f9a.../retry-webhook \
-H "X-API-Key: sk_live_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 auf eine
private/interne Adresse auflöst), und 409, wenn der Job completed nicht erreicht hat.
Code-Beispiele
curl — explizite URL-Liste
curl -X POST https://api.enconvert.com/v2/ingest \
-H "X-API-Key: sk_live_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_live_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 — einreichen, pollen, herunterladen
import time
import requests
BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_live_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 — einreichen und pollen
const BASE = "https://api.enconvert.com";
const HEADERS = {
"Content-Type": "application/json",
"X-API-Key": "sk_live_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]);
}
Plan-Gating
V2-Ingest wird über seinen eigenen Zähler ingest_pages gemessen — getrennt von V1-
Konvertierungen und von den anderen V2-Zählern. Eine Seite wird für jede
URL abgerechnet, deren Render-, Chunk- und JSONL-Stufe abschließt; Seiten, die fehlschlagen oder
übersprungen werden, werden nicht abgerechnet.
Die Prüfung zum Einreichungszeitpunkt ist ein schnelles units=1-Gate: Dein Plan muss Ingest
aktiviert haben, mit etwas monatlichem Spielraum. Der eigentliche Ausgaben-Begrenzer ist die
pro-Seite-Prüfung im Worker — ein crawl-Job stoppt sauber an deiner
monatlichen Obergrenze und markiert die verbleibenden Seiten als skipped, statt sie
zu überlaufen.
| Gate | Verhalten |
|---|---|
| Plan ohne Ingest aktiviert | 402 beim Einreichen — auf einen Plan mit V2-Inklusivleistungen upgraden. |
Monatliche ingest_pages-Quota erschöpft |
402 beim Einreichen; oder, falls mitten im Job erschöpft, werden verbleibende Seiten als skipped markiert. |
| Unbegrenzt (Enterprise / Admin) | Ein Limit von 0 mit aktiviertem Flag bedeutet keine monatliche Obergrenze. |
MAX_PAGES_PER_JOB (1.000) ist eine Obergrenze pro Request, die einen einzelnen
Job begrenzt; es ist nicht deine monatliche Freimenge. Die monatliche ingest_pages-Quota ist
der eigentliche Ausgaben-Begrenzer. Aktuelle Zahlen pro Tarif stehen auf
der Preisseite; siehe auch wie Perceive-Operationen
gemessen werden für den verwandten V2-Zähler.
Fehler-Responses
| Status | Bedingung |
|---|---|
202 Accepted |
Der Job wurde erstellt und eingereiht. Dies 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, oder deine monatliche ingest_pages-Quota 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 aufgerufen für einen Job, der completed nicht erreicht hat. |
400 Bad Request |
retry-webhook aufgerufen ohne konfigurierte webhook_url, oder dessen gespeicherte URL löst nun auf eine private/interne Adresse auf. |
422 Unprocessable Entity |
Quelle passt nicht zum Modus (urls ohne urls, oder eine Seed-url im urls-Modus); ein Parameter liegt 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 dem Support zu nennen ist. |
Ein Render-Fehler auf Seitenebene lässt die Anfrage oder den Job nicht fehlschlagen — er
erhöht pages_failed, schreibt den Fehler der Seite in ihre eigene Zeile, und der
Job läuft weiter. Ein Job fails nur, wenn die Entdeckung abgelehnt wird oder jede Seite
fehlschlägt oder übersprungen wird. Die vollständige Statuscode-Referenz steht in
der Fehlercodes-Anleitung.
Limits
| Limit | Wert |
|---|---|
URLs pro urls-Modus-Request |
1.000 |
url / Länge jedes urls-Eintrags |
2.048 Zeichen |
max_pages (Discovery-Obergrenze) |
1–1.000 |
max_depth |
1–5 |
include_patterns / exclude_patterns |
je 50 |
wait_for-Länge |
1.024 Zeichen |
wait_timeout_ms |
0–60.000 ms |
chunk.max_words |
32–4.000 (Standard 512) |
chunk.sentence_overlap |
0–10 (Standard 1) |
webhook_url-Länge |
2.048 Zeichen |
Seiten-Obergrenze pro Job (MAX_PAGES_PER_JOB) |
1.000 |
GET /v2/ingest-Listen-limit |
1–100 (Standard 20) |
Ablauf der signierten output_url |
15 Minuten |
| Webhook-Zustellversuche | 4 (initial + 3 Retries) |
| Webhook-Replay-Toleranz | 300 Sekunden |
Monatliche ingest_pages |
Planabhängig — siehe Preise |