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.


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.

Der Link ist ein Bearer-Credential. 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.

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.

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.

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

direct_download überspringt den Speicher nicht. Das Artefakt wird zuerst hochgeladen, dann zurückgelesen und zu dir gestreamt. Deshalb antwortet ein Artefakt jenseits seines Aufbewahrungsfensters mit 410 Gone 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.

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

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

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.

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.

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.