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/ingest ist 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.

  1. Queue. POST validiert die Anfrage, führt eine schnelle units=1-Quota- Prüfung durch (Plan hat Ingest aktiviert und etwas monatlichen Spielraum), fügt den Job-Datensatz ein und antwortet mit 202. Es wird nichts persistiert, wenn das Quota-Gate fehlschlägt — ein 402 hinterlässt null Datensätze.
  2. Discover. Für sitemap- und crawl-Modus führt der Worker denselben Entdeckungsdurchlauf wie der Discover-Endpunkt aus, gedeckelt auf max_pages, und prüft die Seed-URL per SSRF-Screening. Für urls-Modus wird die explizite Liste in Reihenfolge dedupliziert; es läuft keine Entdeckung.
  3. 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.
  4. 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.
  5. Assemble. Sobald jede Seite fertig ist, werden die Objekte pro Seite in die finale v2-ingest/{job_id}.jsonl aneinandergehängt, die Staging- Objekte werden gelöscht, der Job wechselt zu completed und — falls eine webhook_url gesetzt 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, keine cookies und keine headers. 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. POST und das GET/DELETE pro Job nutzen response_model_exclude_none, sodass Felder, die noch null sind (wie output_url vor dem Abschluss), aus dem JSON ausgelassen 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. 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