---
seo_title: Sync- vs. Async-Jobs und Polling | EnConvert
meta_desc: Wie EnConvert entscheidet, ob ein Ergebnis inline zurückkommt oder ein Job in die Warteschlange geht, dazu Job-Lebenszyklus, Polling-Vertrag und die Endstatus.
keywords: sync vs async api, async_mode parameter, job status abfragen api, batch status endpunkt, job id timeout wiederherstellung, 202 accepted konvertierung api, endstatus job api, konvertierungsjob status pollen
---

# 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+) |

<div class="alert alert-info">
<strong>Plan-Beschränkung:</strong> Der Async-Modus ist im kostenlosen Plan nicht verfügbar. Der Versuch, <code>async_mode: true</code> zu setzen oder mehrere URLs in einem kostenlosen Plan zu übermitteln, gibt <code>403 Forbidden</code> zurück.
</div>

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](/de/docs/reference/rate-limits.md).

---

## 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](/de/docs/concepts/signed-urls.md). | -- |

Eine minimale Async-Übermittlung:

```bash
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:

```json
{
    "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](/de/docs/endpoints/perceive.md) dokumentiert.

### Async

Eine V1-Async- oder Batch-Übermittlung antwortet mit `202` und sonst nichts:

```json
{
    "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`:

```json
{
    "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_`:

```json
{
    "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"
}
```

<div class="alert alert-warning">
<strong>Der Griff hat zwei Namen.</strong> V1 gibt <code>batch_id</code> zurück. V2 gibt <code>job_id</code> 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.
</div>

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:

```python
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](/de/docs/concepts/signed-urls.md).

Wenn du lieber benachrichtigt werden willst, statt zu fragen, registriere einen Webhook und lass die Schleife ganz weg. [Webhooks](/de/docs/guides/webhooks.md) 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.

1. Erzeuge eine UUID und sende sie als `job_id`, im JSON-Body bei URL-Endpunkten oder als Formularfeld bei Datei-Uploads.
2. Gibt die Anfrage einen 5xx zurück oder bricht die Verbindung ab, übermittle nicht erneut. Frage `GET /v1/convert/status/{job_id}` ab.
3. Höre auf, sobald `status` `success` oder `failed` ist.

```python
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:

```json
{"status": "processing"}
```

```json
{
    "status": "success",
    "presigned_url": "https://spaces.example.com/...signed...",
    "object_key": "env/files/4127/url-to-pdf/report_20260405_123456789.pdf"
}
```

```json
{"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.

<div class="alert alert-info">
<strong>Die SDKs erledigen das für dich.</strong> Jedes offizielle SDK erzeugt pro V1-Konvertierung eine <code>job_id</code>, und gibt der Aufruf einen 5xx zurück, wechselt es stillschweigend zum Abfragen von <code>GET /v1/convert/status/{job_id}</code>, bis der Job <code>success</code> oder <code>failed</code> ist. Du schreibst keinerlei Wiederherstellungscode. Siehe <a href="/de/docs/guides/integrations/sdks">SDKs</a>.
</div>

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-pdf` und `website-to-screenshot` crawlen eine Site und bündeln die Ausgabe in ein ZIP. Sie sind ausschließlich async und antworten immer mit `202`.
- **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](/de/docs/reference/rate-limits.md), 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](/de/docs/guides/batch-processing.md).

---

## 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.
