---
seo_title: Signierte Download-URLs: Ablauf und Aufbewahrung | EnConvert
meta_desc: Jede EnConvert-Ausgabe kommt als zeitlich begrenzte signierte URL. Was die URL enthält, wie lange sie gilt und wie du die Bytes stattdessen direkt streamst.
keywords: presigned url ablauf, signierte download url api, direct_download parameter, aufbewahrungsdauer dateien api, s3 presigned url 15 minuten, download url neu signieren, konvertierte datei herunterladen api, 410 gone artefakt abgelaufen
---

# Signierte Download-URLs

EnConvert legt deine konvertierte Datei standardmäßig nicht in den
Response-Body. Die Datei wird in den Objektspeicher hochgeladen, und du
bekommst eine signierte URL zurück: einen gewöhnlichen HTTPS-Link, der seine
Autorisierung im Query-String mitführt und 15 Minuten nach der Ausstellung
aufhört zu funktionieren.

---

## Warum die Ausgabe ein Link ist

Zwei Gründe, beide praktisch.

Die Antwort bleibt klein. Eine Konvertierungsantwort sind ein paar hundert
Bytes JSON, egal wie schwer die Ausgabe ist, dein Client parst also eine
einzige vorhersagbare Form, ob das Ergebnis nun eine 4 KB große
Markdown-Datei oder ein 140 MB großes ZIP einer ganzen Website ist. Es
bedeutet außerdem, dass eine Batch-Status-Antwort 400 Ergebnisse tragen kann,
ohne 400 Dateien zu tragen.

Die Bytes kommen aus dem Speicher, nicht aus der API. Downloads liefert die
Speicherschicht direkt aus, ein langsamer Client, der ein großes PDF zieht,
hält also keinen API-Worker offen und sitzt nicht hinter denselben
Proxy-Timeouts, die eine Konvertierungsanfrage begrenzen. Der Link braucht
keinen `X-API-Key`-Header, und genau deshalb kannst du ihn bedenkenlos an
einen Browser, einen Queue-Consumer oder ein `curl` in einem Shell-Skript
weitergeben.

<div class="alert alert-warning">
<strong>Der Link ist ein Bearer-Credential.</strong> Wer die URL hat, kann diese Datei herunterladen, bis sie abläuft. Es gibt keine zweite Prüfung des API-Schlüssels. Behandle eine signierte URL wie ein Passwort mit 15 Minuten Lebensdauer: logge sie nicht, stelle sie nicht in einen öffentlichen Issue-Tracker und poste sie nicht in einen geteilten Kanal.
</div>

---

## Wie die URL aussieht

Es ist eine standardmäßige AWS-SigV4-GET-URL im Path-Style gegen den
Storage-Host:

```
https://<region>.digitaloceanspaces.com/<bucket>/<object_key>
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=<key>%2F<date>%2F<region>%2Fs3%2Faws4_request
  &X-Amz-Date=<timestamp>
  &X-Amz-Expires=900
  &X-Amz-SignedHeaders=host
  &X-Amz-Signature=<hex>
```

Host und Bucket hängen vom Deployment ab, lies sie also aus der URL, die du
bekommen hast, statt sie fest zu verdrahten. Die Form ändert sich nicht: ein
einfaches GET, keine Header nötig, `X-Amz-Expires=900`.

Der Object Key darin ist deterministisch und liegt in einem eigenen Namespace
pro Projekt:

```
{env}/files/{project_id}/{endpoint}/{filename}
live/files/4127/v2-perceive/per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7_markdown.md
```

Das Signieren ist auf dieses Präfix beschränkt. Ein Projekt kann immer nur
eine Signatur über seine eigenen Keys erhalten, ein `object_key` aus dem
Projekt einer anderen Person lässt sich also nicht in eine funktionierende
URL verwandeln.

---

## Ablauf: 15 Minuten, V1 und V2

Jede signierte URL, die EnConvert ausstellt, lebt 900 Sekunden. Es gibt
keinen Request-Parameter, um sie länger oder kürzer zu machen.

| Wo sie auftaucht | Feld |
|------------------|-------|
| V1 synchrone Konvertierungsantwort | `presigned_url` |
| V1 Batch-Status, pro Element | `download_url` |
| V1 Batch-Status, ZIP-Modus | `zip_download_url` |
| V1 Job-Status-Abfrage | `presigned_url` |
| V2 perceive, pro Ausgabe | `outputs.<name>.url`, zusammen mit `expires_in: 900` |
| V2 perceive-Batch, ZIP-Modus | `zip.url` |
| V2 ingest, bei Abschluss | `output_url` |

Signierte URLs sind wiederverwendbar, nicht einmalig. Dieselbe URL
funktioniert für wiederholte GETs weiter, bis die 15 Minuten abgelaufen sind.
Nichts macht sie vorzeitig ungültig, und einmaliges Herunterladen verbraucht
sie nicht.

Brauchst du die Datei länger als 15 Minuten, lade die Bytes herunter und
speichere sie selbst. Neu signieren gibt dir einen frischen Link, keinen
dauerhaften Zugriff.

---

## Neu signieren

Ein abgelaufener Link ist keine verlorene Datei. Frag die API erneut, und sie
erzeugt eine neue Signatur über dasselbe gespeicherte Objekt:

| Job | Neu signieren mit |
|-----|--------------|
| V1 asynchrone oder Batch-Konvertierung | `GET /v1/convert/batch/{batch_id}` |
| V1 synchrone Konvertierung, per eigener ID abgefragt | `GET /v1/convert/status/{job_id}` |
| V2 perceive-Operation | `GET /v2/perceive/{operation_id}` |
| V2 perceive-Batch | `GET /v2/perceive/batch/{job_id}` |
| V2 ingest-Job | `GET /v2/ingest/{job_id}` |

Jeder dieser Endpunkte baut die URLs bei jedem Aufruf neu aus den
persistierten Object Keys auf. Neu signieren rendert nichts, konvertiert
nichts und kostet keine Ops. Es funktioniert, solange das Objekt noch im
Speicher liegt, und das ist die andere Uhr auf dieser Seite.

Ein Verhalten, für das du Code schreiben solltest: Lässt sich die Signatur
nicht erzeugen, kommt das Feld als `null` zurück, statt dass die Anfrage
fehlschlägt. Eine Status-Abfrage liefert wegen eines veralteten Keys nie eine
500. Prüfe also `url`, `download_url` und `output_url` auf `null`, bevor du
sie dereferenzierst.

---

## Aufbewahrung ist eine andere Uhr

Das ist die Unterscheidung, die am häufigsten falsch verstanden wird, hier
also je eine Zeile:

- **Der Signatur-Ablauf (15 Minuten)** entscheidet, wie lange eine bestimmte
  URL funktioniert.
- **Die Aufbewahrung (Stunden bis Tage, je nach Plan)** entscheidet, wie
  lange die Datei überhaupt existiert.

Sie sind voneinander unabhängig, und deshalb sind beide verwirrenden Fälle
real:

**Eine abgelaufene URL heißt nicht, dass die Datei weg ist.** Fünfzehn
Minuten nach einer Konvertierung ist der Link tot, das Objekt aber mit
ziemlicher Sicherheit noch da. Frag den Job erneut ab, und du bekommst einen
funktionierenden Link zurück.

**Eine gültige URL garantiert nicht, dass die Datei noch da ist.** Signierst
du eine Ausgabe im Founding-Plan nach 59 Minuten neu und nutzt den Link nach
62 Minuten, kann der Aufbewahrungs-Sweep das Objekt dazwischen gelöscht
haben. Die Signatur ist gültig; das Objekt nicht. Der Download scheitert in
der Speicherschicht, nicht in der API.

Die Aufbewahrungsdauer wird pro Plan über dein Abo gesetzt, und das Fenster
des Founding-Plans beträgt eine Stunde, kurz genug, um es während der
Entwicklung versehentlich zu treffen. Die Tabelle pro Plan steht in
[Rate-Limits und Kontingente](/de/docs/reference/rate-limits.md).

Zwei weitere Dinge zur Aufbewahrung, die du kennen solltest:

- Das Löschen wird eingeplant und dann in Intervallen abgearbeitet, eine
  Datei kann ihr Fenster also um ein paar Minuten überleben. Baue nichts
  darauf. Das ist Spiel im Sweeper, keine Kulanzfrist.
- Projekte mit einem Storage-Add-on bekommen überhaupt keine geplanten
  Löschungen. Ihre Ausgaben bleiben, bis sie bewusst entfernt werden, und
  zählen stattdessen gegen das Speicherkontingent des Add-ons.

Unabhängig vom Fenster deines Plans werden gerenderte HTML-Captures, die für
das Render-Quality-Scoring genutzt werden, 90 Tage aufbewahrt und lassen sich
pro Anfrage mit dem Header `X-Enconvert-No-Capture: true` abschalten.
Quelldateien, die an `POST /v2/ingest/files` hochgeladen werden, werden
gelöscht, sobald das JSONL zusammengebaut ist, mit einer 24-Stunden-Absicherung
für den Fall, dass vorher etwas schiefgeht.

---

## direct_download: die Bytes stattdessen streamen

Wenn dir das Folgen einer URL ein Hop zu viel ist, fordere die Bytes im
Response-Body an.

### Bei V2 perceive

`direct_download: true` bei `POST /v2/perceive` ersetzt den JSON-Envelope
vollständig. Der Response-Body ist das Artefakt, ausgeliefert mit dem eigenen
Content-Type des Artefakts.

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown"],
    "direct_download": true
  }' \
  --output pricing.md
```

Es braucht genau eine artefakterzeugende Ausgabe: `markdown`,
`html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`,
`links` oder `images`, die alle auf
[der perceive-Seite](/de/docs/endpoints/perceive.md) beschrieben sind. Forderst
du zwei an, antwortet der Aufruf mit `400` und listet auf, was du gesendet
hast. `structured` zählt nicht, weil es inline-JSON und keine gespeicherte
Datei ist, es darf also mitfahren, ohne die Regel zu brechen.

Die Metadaten, die im JSON-Body gestanden hätten, wandern in Header:
`X-Operation-Id`, `X-Object-Key` und `X-Cache-Hit` immer, dazu
`X-Render-Quality`, `X-Source-Status-Code`, `X-Content-Hash` und
`X-Warnings-Count`, sofern diese Werte existieren. Der Body trägt außerdem
`Content-Disposition: attachment`, `Content-Length` und
`Cache-Control: no-transform`.

Zwei GET-Endpunkte nehmen `direct_download` als Query-Parameter:

- `GET /v2/perceive/{operation_id}?direct_download=true&output=markdown`
  streamt ein Artefakt aus einer vergangenen Operation. `output` ist
  erforderlich, wenn die Operation mehr als ein Artefakt erzeugt hat, und ein
  unbekannter Name liefert `404`.
- `GET /v2/perceive/batch/{job_id}?direct_download=true` streamt das
  Batch-ZIP für Batches mit `output_mode: "zip"`, deren Archiv fertig ist,
  und antwortet sonst mit `400`.

`POST /v2/perceive/batch` lehnt `direct_download` mit `422` ab. Setze
`output_mode` auf `"zip"` und lade stattdessen das Archiv herunter.

<div class="alert alert-info">
<strong>direct_download überspringt den Speicher nicht.</strong> Das Artefakt wird zuerst hochgeladen, dann zurückgelesen und zu dir gestreamt. Deshalb antwortet ein Artefakt jenseits seines Aufbewahrungsfensters mit <code>410 Gone</code> und einer Meldung, die dich auffordert, die Anfrage erneut auszuführen, statt still leere Bytes zurückzugeben. Gespart wird ein Roundtrip, kein Schreibvorgang im Speicher.
</div>

### Bei V1-Konvertierungen

V1 hat ebenfalls ein `direct_download`-Feld, mit anderem Verhalten als dem
von V2. Auf den URL-Endpunkten entscheidet es, ob die API rohe Datei-Bytes
oder einen JSON-Body mit einer vorsignierten Download-URL zurückgibt. Auf den
Datei-Upload-Endpunkten wird es nur akzeptiert, damit die Request-Form passt,
und bleibt wirkungslos: Diese Endpunkte antworten immer mit dem JSON-Body.

| Endpunkt-Typ | Schlüsseltyp | Standard | Was zurückkommt |
|---|---|---|---|
| Datei-Upload-Endpunkte | Alle Schlüssel | `true` | JSON mit `presigned_url`, egal was du setzt. Das Feld ist hier wirkungslos. |
| URL-Endpunkte | Privater Schlüssel | `false` | JSON mit `presigned_url`. Setze `true` für rohe Bytes. |
| URL-Endpunkte | Öffentlicher Schlüssel / Dashboard-Schlüssel | `true` (erzwungen) | JSON mit `presigned_url` |

<div class="alert alert-info">
<strong>Verhalten öffentlicher Schlüssel:</strong> Bei öffentlichen und Dashboard-Schlüsseln wird <code>direct_download</code> auf <code>true</code> erzwungen, die Antwort ist aber ein JSON-Objekt mit einer <code>presigned_url</code> (keine rohen Bytes). Das vermeidet Reverse-Proxy-Timeout-Probleme bei großen Dateien, deren Konvertierung 60 bis 120 Sekunden dauern kann.
</div>

Auf einem URL-Endpunkt liefert ein privater Schlüssel mit `direct_download=true`
rohe Datei-Bytes:

```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", "direct_download": true}' \
  --output output.pdf
```

Einschränkungen:

- `direct_download` lässt sich nicht mit `async_mode: true` kombinieren
  (liefert `400`)
- `direct_download` lässt sich nicht mit mehreren URLs kombinieren (liefert
  `400`)

Beide Einschränkungen folgen aus derselben Tatsache: Sobald der Aufruf mit
`202` geantwortet hat, ist keine Antwort mehr übrig, in die die Bytes passen.
Asynchrone Ergebnisse holst du per Polling oder Webhook ab. Siehe
[Synchrone und asynchrone Jobs](/de/docs/concepts/sync-and-async.md).

### Response-Header

Konvertierungen per Datei-Upload und URL-Konvertierungen mit einem
öffentlichen oder Dashboard-Schlüssel wiederholen die Konvertierungs-Metadaten
zusätzlich zum JSON-Body in Headern:

| Header | Beschreibung |
|--------|-------------|
| `X-Object-Key` | Speicherpfad der konvertierten Datei |
| `X-File-Size` | Größe der konvertierten Datei in Bytes |
| `X-Conversion-Time` | Dauer der Konvertierung in Sekunden |
| `X-Filename` | Generierter Dateiname |

Eine Antwort mit rohen Bytes (URL-Endpunkt, privater Schlüssel,
`direct_download: true`) trägt diese vier plus
`Content-Disposition: attachment; filename="{filename}"`, `Content-Length` und
`Cache-Control: no-transform`. Eine URL-Konvertierung, die einem privaten
Schlüssel JSON zurückgibt, trägt keinen davon; lies dort den Body.

---

## Häufig gestellte Fragen

### Wie lange bleiben signierte Download-URLs von EnConvert gültig?

Fünfzehn Minuten bzw. 900 Sekunden, bei V1 wie bei V2. Der
V2-Artefakt-Envelope gibt es explizit als `expires_in: 900` an. Es gibt
keinen Parameter, um das zu verlängern. Ruf den Job oder die Operation erneut
ab, um eine frische URL über dieselbe Datei zu erzeugen.

### Kann ich eine signierte URL mehr als einmal verwenden?

Ja. Signierte URLs sind nicht einmalig. Derselbe Link bedient wiederholte
GETs, bis die 15 Minuten abgelaufen sind, und ein Download macht ihn nicht
ungültig.

### Meine Download-URL ist abgelaufen. Wurde die Datei gelöscht?

Mit ziemlicher Sicherheit nicht. Signatur-Ablauf und Datei-Aufbewahrung sind
getrennte Uhren. Frag den Job oder die Operation erneut ab
(`GET /v1/convert/batch/{batch_id}`, `GET /v2/perceive/{operation_id}`,
`GET /v2/ingest/{job_id}`), und du bekommst einen frisch signierten Link ohne
Op-Kosten, solange die Datei noch im Aufbewahrungsfenster deines Plans liegt.

### Wie lange bewahrt EnConvert meine konvertierten Dateien auf?

Die Aufbewahrung wird pro Plan gesetzt, und der Founding-Plan behält Ausgaben
eine Stunde lang. Bezahlte Pläne behalten sie länger, und Projekte mit einem
Storage-Add-on werden überhaupt nie geleert. Die Zahlen pro Plan stehen in
[Rate-Limits und Kontingente](/de/docs/reference/rate-limits.md).

### Was bedeutet ein 410 Gone bei einem perceive-Artefakt?

Das Objekt hat das Aufbewahrungsfenster deines Plans überschritten und wurde
aus dem Speicher gelöscht. `direct_download` liest das Artefakt vor dem
Streamen aus dem Speicher zurück, ein herausgefallenes Artefakt antwortet
deshalb mit `410` statt mit einem leeren Body. Führe die perceive-Anfrage
erneut aus, um es neu zu erzeugen.

### Wie bekomme ich rohe Bytes statt einer Download-URL?

Setze `direct_download: true`. Bei `POST /v2/perceive` verlangt das genau
eine artefakterzeugende Ausgabe, und der Response-Body wird zu diesem
Artefakt. Bei V1-URL-Endpunkten mit einem privaten Schlüssel liefert es die
Datei-Bytes, und es lässt sich nicht mit `async_mode` oder mit mehreren URLs
kombinieren.
