Synchrone und asynchrone Jobs#
Die meisten EnConvert-Aufrufe geben dir das fertige Ergebnis im Response-Body zurück. Manche geben dir stattdessen eine ID und erledigen die Arbeit im Hintergrund. Was du bekommst, hängt vom aufgerufenen Endpunkt ab und, bei einigen Endpunkten, davon, was du in die Anfrage schreibst.
Was den Modus bestimmt#
| Endpunkt | Modus |
|---|---|
| Alle Datei-Upload-Konvertierungen (Dokumente, Datenformate, Bilder) | Immer sync. async_mode wird an diesen Endpunkten nie gelesen. |
url-to-pdf, url-to-screenshot, url-to-markdown |
Standardmäßig sync. Async, wenn du async_mode: true setzt oder wenn url ein Array ist. |
website-to-pdf, website-to-screenshot |
Immer async. Beide antworten mit 202, einer batch_id und output_format: "zip". |
POST /v2/perceive |
Immer sync. Eine einzelne URL wird innerhalb der Anfrage gerendert, und einen Async-Schalter gibt es nicht. |
POST /v2/perceive/batch |
Sync bei 10 URLs oder weniger, darüber async. |
POST /v2/ingest, POST /v2/ingest/files |
Immer async. Beide antworten mit 202 und einer job_id. |
Öffentliche und Dashboard-Schlüssel sind an den V1-URL-Endpunkten auf synchrone Anfragen mit einer einzelnen URL beschränkt, unabhängig davon, was im Body steht.
| Sync-Modus | Async-Modus | |
|---|---|---|
| Auslöser | Standard für einzelne URL / Datei-Upload | Mehrere URLs oder async_mode: true |
| Antwort | 200 OK mit Ergebnis |
202 Accepted mit batch_id |
| Ergebnislieferung | Datei-Bytes oder presigned URL in der Antwort | Polling, Webhook oder E-Mail |
| Schlüsseltypen | Private und öffentliche Schlüssel | Nur private Schlüssel |
| Plan-Anforderung | Alle Pläne | Erfordert Async-Zugriff (Indie+) |
async_mode: true zu setzen oder mehrere URLs in einem kostenlosen Plan zu übermitteln, gibt 403 Forbidden zurück.
Async und Batch gehören beide zu den bezahlten Plänen. Der Founding-Plan hat weder das eine noch das andere, weshalb ein erster Test mit einem kostenlosen Schlüssel, der drei URLs übermittelt, 403 statt 202 zurückgibt. Die Zahlen je Plan, einschließlich der Obergrenze für die Batch-Größe, stehen in Rate-Limits und Kontingente.
Async explizit anfordern#
Das sind die Request-Felder, die den Modus bestimmen oder dir einen Griff auf den entstehenden Job geben. Alles Übrige an der Anfrage (Rendering-Optionen, PDF-Optionen, Ausgabe-Benennung) ist in beiden Modi identisch.
| Parameter | Typ | Standard | Beschreibung | Plan-Beschränkung |
|---|---|---|---|---|
async_mode |
boolean |
false |
Stellt die Arbeit in die Warteschlange und antwortet mit 202, statt die Verbindung offen zu halten. Wird nur von url-to-pdf, url-to-screenshot und url-to-markdown gelesen. |
Erfordert Async-Zugriff |
url (Array) |
string[] |
-- | Mehr als eine URL erzwingt async_mode auf true, ob du es setzt oder nicht, und wird gegen das Batch-Limit deines Plans geprüft. |
Erfordert Batch-Zugriff |
job_id |
string |
null |
Eine ID, die du selbst erzeugst; damit holst du das Ergebnis zurück, falls die Anfrage selbst stirbt. Wird an URL-Endpunkten im JSON-Body gesendet und an Datei-Upload-Endpunkten als Formularfeld. Funktioniert mit jedem Schlüsseltyp. | -- |
callback_url |
string |
null |
Webhook-URL, die bei Abschluss einen POST erhält. | Erfordert Webhook-Zugriff |
notification_email |
string |
E-Mail des Projekt-Inhabers | E-Mail-Adresse, die bei Abschluss benachrichtigt wird. Ohne Angabe wird die E-Mail-Adresse des Projekt-Inhabers verwendet. | -- |
direct_download |
boolean |
Endpunktabhängig | Kann nicht mit async_mode: true oder mit mehreren URLs kombiniert werden. Beide Kombinationen geben 400 zurück. Siehe Signierte URLs. |
-- |
Eine minimale Async-Übermittlung:
curl -X POST https://api.enconvert.com/v1/convert/url-to-pdf \
-H "X-API-Key: sk_your_private_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/very-long-report", "async_mode": true}'
Was in jedem Modus zurückkommt#
Sync#
Eine V1-Konvertierung, die innerhalb der Anfrage fertig wird, antwortet mit 200, den Metadaten und einem signierten Link auf die Ausgabe:
{
"presigned_url": "https://spaces.example.com/...signed...",
"object_key": "env/files/4127/url-to-pdf/example_20260405_123456789.pdf",
"filename": "example_20260405_123456789.pdf",
"file_size": 184320,
"conversion_time_seconds": 3.12
}
Öffentliche und Dashboard-Schlüssel bekommen dieselben fünf Felder plus job_id sowie dieselben Werte gespiegelt in den Response-Headern X-Object-Key, X-File-Size, X-Conversion-Time und X-Filename.
POST /v2/perceive ist ebenfalls sync, aber sein Body ist das vollständige perceive-Ergebnis: operation_id, status, render_quality, eine outputs-Map signierter Artefakte und der inline eingebettete structured-Block. Diese Form ist auf der perceive-Seite dokumentiert.
Async#
Eine V1-Async- oder Batch-Übermittlung antwortet mit 202 und sonst nichts:
{
"status": "processing",
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"url_count": 3,
"output_format": "individual"
}
Ein perceive-Batch, der für die Inline-Ausführung zu groß ist, antwortet mit 202 und einer job_id:
{
"job_id": "bat_8c1a...",
"status": "queued",
"output_mode": "manifest",
"total": 40,
"completed": 0,
"failed": 0,
"pending": 40
}
Der vollständige Batch-Body führt außerdem zip, items und warnings. Eine Ingest-Übermittlung antwortet mit 202 und einer ID mit dem Präfix ing_:
{
"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"
}
batch_id zurück. V2 gibt job_id zurück. Dahinter steckt dieselbe Idee (ein undurchsichtiger String, mit dem du abfragst), aber es sind unterschiedliche Felder an unterschiedlichen Endpunkten, und nichts übersetzt zwischen ihnen. Lies das Feld, das der von dir aufgerufene Endpunkt tatsächlich zurückgibt.
Es gibt noch einen Fall, den du kennen solltest. Ein perceive-Batch mit 10 URLs oder weniger läuft normalerweise inline und antwortet mit 200 und vollständig gefüllten Items. Überschreitet dieser Inline-Lauf jedoch sein Wartefenster von 240 Sekunden, fällt er auf ein 202 mit status: "processing" und einer Warnung zurück, die dir sagt, dass du abfragen sollst. Behandle 202 also bei jedem Batch-Aufruf als möglich, nicht nur bei großen.
Status-Endpunkte und Endstatus#
| Job | Abfrage | Nicht-terminal | Terminal |
|---|---|---|---|
| V1-Async- oder Batch-Konvertierung | GET /v1/convert/batch/{batch_id} |
processing |
completed, partial, failed |
V1-Sync-Konvertierung mit eigener job_id |
GET /v1/convert/status/{job_id} |
processing |
success, failed |
| V2-perceive-Batch | GET /v2/perceive/batch/{job_id} |
queued, processing |
completed, partial, failed, canceled |
| V2-Ingest | GET /v2/ingest/{job_id} |
queued, discovering, processing |
completed, failed, canceled |
partial bedeutet, dass der Job abgeschlossen ist und einzelne Einheiten fehlgeschlagen sind. Das ist ein Endstatus. Behandle ihn nicht für sich genommen als Retry-Signal; lies die Zeilen je Item und wiederhole nur die Fehlschläge.
Innerhalb eines V2-perceive-Batches trägt jedes Item seinen eigenen status: queued, processing, completed oder failed. Auf Item-Ebene gibt es kein partial und kein canceled, nur auf Batch-Ebene.
Die V1-Batch-Antwort mischt die Schreibweisen: Der aggregierte status ist kleingeschrieben (processing, completed, partial, failed), während der status jedes Items in Title Case steht (Success, In Progress, Failed). Vergleiche exakt oder normalisiere vor dem Vergleich.
Beide V2-Job-Typen lassen sich abbrechen: DELETE /v2/perceive/batch/{job_id} und DELETE /v2/ingest/{job_id}. Beide sind idempotent, beide stoppen den Worker zwischen zwei Einheiten, und bereits fertige Arbeit behält ihre Artefakte.
Der Polling-Vertrag#
Die API sagt dir nicht, wie schnell du abfragen sollst. Ein 202 trägt keinen Retry-After-Header, und im Body steht kein empfohlenes Intervall. Der Vertrag lautet nur: Das 202 liefert die ID, du machst ein GET auf den passenden Status-Endpunkt, und du hörst auf, sobald status einen Endwert erreicht.
Was du in der Praxis nimmst:
- Fünf Sekunden sind ein vernünftiger Standard für V1-Batches und für Ingest-Jobs. Beide verbringen den größten Teil ihrer Laufzeit mit Browser-Renders, die rund 10 bis 30 Sekunden pro Seite dauern; schneller abzufragen bringt dir also vor allem zusätzliche Requests.
- Drei Sekunden verwenden die offiziellen SDKs für die Timeout-Wiederherstellung bei einer einzelnen Konvertierung, wo die Antwort meist nur Sekunden entfernt ist.
- Setze eine Frist. Die SDKs warten standardmäßig 30 Minuten bei Batches über ganze Websites und 5 Minuten bei der Timeout-Wiederherstellung.
- Status-Abfragen sind GETs. Der Rate-Limiter greift ausschließlich bei POST-Requests, Polling zählt also nicht gegen dein Limit pro Minute, und das Lesen eines Status kostet keine Op.
Eine Polling-Schleife gegen einen Ingest-Job:
import time
import requests
HEADERS = {"X-API-Key": "sk_your_private_key"}
TERMINAL = {"completed", "failed", "canceled"}
job = requests.post(
"https://api.enconvert.com/v2/ingest",
headers=HEADERS,
json={"mode": "sitemap", "url": "https://example.com", "max_pages": 200},
).json()
while True:
status = requests.get(
f"https://api.enconvert.com/v2/ingest/{job['job_id']}",
headers=HEADERS,
).json()
print(status["status"], status["pages_processed"], "pages")
if status["status"] in TERMINAL:
break
time.sleep(5)
if status["status"] == "completed":
print(status["output_url"]) # signed for 15 minutes
Jede Abfrage prägt einen frischen Satz signierter Download-URLs auf dieselben gespeicherten Objekte; ein Link, der abgelaufen ist, während du gelesen hast, wird also durch erneutes Abfragen einfach ersetzt. Das behandelt Signierte URLs.
Wenn du lieber benachrichtigt werden willst, statt zu fragen, registriere einen Webhook und lass die Schleife ganz weg. Webhooks beschreibt die Payloads, das Signaturverfahren und die Retry-Policy.
Timeout-Wiederherstellung: sende deine eigene Job-ID#
Lange Konvertierungen haben ein Verbindungsproblem, kein Verarbeitungsproblem. Ein schwerer Seiten-Render oder ein großes Dokument kann den Reverse-Proxy vor der API überdauern (typischerweise 60 bis 120 Sekunden), und das Gateway selbst bricht jede Anfrage ab, die nicht innerhalb von 300 Sekunden zu antworten begonnen hat, und gibt 504 mit {"error": "Request timeout"} zurück. In beiden Fällen wird die Konvertierung auf dem Server oft trotzdem fertig. Das Ergebnis existiert. Nur deine Verbindung hat es nicht mehr erlebt.
Die Lösung: Benenne den Job, bevor du ihn startest.
- Erzeuge eine UUID und sende sie als
job_id, im JSON-Body bei URL-Endpunkten oder als Formularfeld bei Datei-Uploads. - Gibt die Anfrage einen 5xx zurück oder bricht die Verbindung ab, übermittle nicht erneut. Frage
GET /v1/convert/status/{job_id}ab. - Höre auf, sobald
statussuccessoderfailedist.
import time
import uuid
import requests
HEADERS = {"X-API-Key": "sk_your_private_key"}
job_id = str(uuid.uuid4())
response = requests.post(
"https://api.enconvert.com/v1/convert/url-to-pdf",
headers=HEADERS,
json={"url": "https://example.com/heavy-report", "job_id": job_id},
)
if response.status_code >= 500:
while True:
status = requests.get(
f"https://api.enconvert.com/v1/convert/status/{job_id}",
headers=HEADERS,
).json()
if status["status"] != "processing":
break
time.sleep(3)
else:
status = response.json()
Der Status-Endpunkt antwortet immer mit 200 und einem von drei Bodies; prüfe also das Feld status statt des HTTP-Codes:
{"status": "processing"}
{
"status": "success",
"presigned_url": "https://spaces.example.com/...signed...",
"object_key": "env/files/4127/url-to-pdf/report_20260405_123456789.pdf"
}
{"status": "failed", "error": "Page load timeout"}
Eine unbekannte ID gibt 404 zurück, und eine ID, die einem anderen Projekt gehört, gibt 403 zurück. Eine eigene ID wiederzuverwenden setzt die betreffende Job-Zeile zurück, wähle also pro Anfrage eine frische UUID; beanspruchst du eine ID, die bereits ein anderes Projekt hält, kommt 409 mit job_id already in use zurück.
job_id, und gibt der Aufruf einen 5xx zurück, wechselt es stillschweigend zum Abfragen von GET /v1/convert/status/{job_id}, bis der Job success oder failed ist. Du schreibst keinerlei Wiederherstellungscode. Siehe SDKs.
V2 braucht diesen Trick nicht. Seine langlaufende Arbeit gibt bereits ein explizites Job-Objekt zurück, du fragst also stattdessen GET /v2/perceive/batch/{job_id} oder GET /v2/ingest/{job_id} ab.
Wann Async die einzig vernünftige Wahl ist#
Manche Jobs passen nicht in eine Anfrage, und die API tut auch nicht so, als ob:
- Renders ganzer Websites.
website-to-pdfundwebsite-to-screenshotcrawlen eine Site und bündeln die Ausgabe in ein ZIP. Sie sind ausschließlich async und antworten immer mit202. - Ingest. Jede Seite in einem Ingest-Job durchläuft einen echten Browser-Render von jeweils rund 10 bis 30 Sekunden, jeder nicht triviale Crawl liegt also jenseits des 300-Sekunden-Fensters der Anfrage, bevor er halb fertig ist. Beide Ingest-Einstiegspunkte antworten konstruktionsbedingt mit
202. - Perceive-Batches über 10 URLs. Zehn ist die Obergrenze für die Inline-Ausführung. Darüber bekommst du einen Job.
- Alles, wofür du lieber keinen Socket offen hältst. Ein Batch mit 40 URLs lässt sich technisch in einer Schleife abfragen, aber ein Webhook plus eine Queue auf deiner Seite übersteht deine eigenen Deploys und Neustarts. Batch-Jobs überstehen einen Neustart des Gateways und laufen weiter, du übermittelst also nie erneut.
Datei-Upload-Konvertierungen sind die Ausnahme von alldem. Sie haben überhaupt keinen Async-Modus, eine langsame Dokumentkonvertierung holst du also mit job_id-Polling zurück und nicht mit async_mode. Ist die Datei selbst das Problem, prüfe die Upload-Obergrenze deines Plans in Rate-Limits und Kontingente, bevor du auf ein Timeout schließt.
Die vollständige Form der Batch-Anfrage, das ZIP-Bündeln und die Ergebnisse je Item behandelt Batch-Verarbeitung.
Häufig gestellte Fragen#
Wie mache ich eine EnConvert-Konvertierung asynchron?#
Setze async_mode: true im JSON-Body von url-to-pdf, url-to-screenshot oder url-to-markdown, oder übergib ein Array von URLs, was async von sich aus erzwingt. Der Aufruf antwortet mit 202 und einer batch_id, die du unter GET /v1/convert/batch/{batch_id} abfragst. Datei-Upload-Endpunkte lesen async_mode nie und laufen immer synchron.
Welche Endstatus hat ein EnConvert-Job?#
Ein V1-Batch endet auf completed, partial oder failed. Ein V2-perceive-Batch endet auf completed, partial, failed oder canceled. Ein V2-Ingest-Job endet auf completed, failed oder canceled. Alles andere (processing, queued, discovering) heißt: weiter abfragen.
Wie oft sollte ich einen Job-Status-Endpunkt abfragen?#
Die API gibt keinen Takt vor und sendet keinen Retry-After-Header. Fünf Sekunden sind ein sinnvoller Standard für Batches und Ingest-Jobs, da jeder Seiten-Render rund 10 bis 30 Sekunden dauert. Status-Abfragen sind GETs, liegen also außerhalb des Rate-Limiters und kosten keine Ops, trotzdem gibt es keinen Grund, alle 200 ms abzufragen.
Meine Konvertierungsanfrage lief in ein Timeout. Ist die Datei verloren?#
Meistens nicht. Hast du eine eigene job_id gesendet, frage GET /v1/convert/status/{job_id} ab: Die Konvertierung wird auf dem Server oft fertig, nachdem die Verbindung längst abgebrochen ist. Der Endpunkt antwortet mit 200 und processing, success oder failed. Jedes offizielle SDK führt diese Wiederherstellung automatisch durch.
Warum gibt mein Batch batch_id zurück, die Doku spricht aber von job_id?#
Beides existiert. V1-Konvertierungsendpunkte geben batch_id zurück und werden unter GET /v1/convert/batch/{batch_id} abgefragt. V2-Endpunkte geben job_id zurück und werden unter GET /v2/perceive/batch/{job_id} oder GET /v2/ingest/{job_id} abgefragt. Lies das Feld, das der von dir aufgerufene Endpunkt zurückgegeben hat.