Patu

Patu API-Referenz

Jede Anfrage wird mit einem X-Api-Key-Header authentifiziert. Die Basis-URL ist https://patu.dev. Schicke die rohen Asset-Bytes als Request-Body. Patu optimiert Bilder, SVG, JavaScript, CSS und Schriften; Nicht-Bild-Assets werden anhand des Content-Type der Anfrage geroutet (siehe Nicht-Bild-Assets). EPUB-Bücher, CBZ/CBR-Comic-Archive und Office-Dokumente werden als gesamter Container optimiert (siehe EPUB-Optimierung, CBZ/CBR-Optimierung, Office-Optimierung, PDF-Optimierung).

POST /v1/compress

Komprimiert ein Bild. Liefert die Bytes zurück (inline, Standard) oder ein JSON-Manifest (gespeichert).

ParameterStandardBedeutung
modeinlineinline liefert die Bytes der gewählten Variante zurück; stored speichert jede Variante dauerhaft und liefert ein Manifest zurück.
formatsavif,webp,fallbackKommagetrennte Liste. fallback wird zu JPEG für Fotos aufgelöst, verlustfreiem WebP/PNG für Grafiken oder Transparenz.
widthsOriginalKommagetrennte Liste von Pixelbreiten. Nie über die Quellbreite hinaus hochskaliert.
targetje SchlüsselDer anzustrebende ssimulacra2-Wert (höher = näher am Original, größere Datei). Typisch 75–85.

Im Inline-Modus berücksichtigt das zurückgegebene Format auch den Accept-Header, wenn formats nicht gesetzt ist.

Inline-Antwort

Die Bild-Bytes, plus Header, die das Ergebnis beschreiben:

X-Patu-Formatgewähltes Übertragungsformat (avif/webp/jpeg oder js/css/woff2/svg)
X-Patu-Original-Bytes / X-Patu-Output-BytesEingabe- und Ausgabegröße
X-Patu-Encoded-Bytesbei JS/CSS die Größe nach der Kompression durch den Edge (Brotli/gzip): die tatsächlich übertragene Menge
X-Patu-Latency-Msserverseitige Verarbeitungszeit dieser Anfrage, in Millisekunden
X-Patu-IntegritySubresource-Integrity-Token (sha384-…) der ausgelieferten Bytes, einsatzbereit für <script>/<link> integrity="…"
X-Patu-Scoressimulacra2-Wert (gemessen oder erwartet auf dem schnellen Pfad)
X-Patu-Verifiedtrue, wenn der Wert bei dieser Anfrage gemessen wurde, false, wenn er vom Learner vorhergesagt wurde
X-Patu-Cachehit oder miss im prozessinternen Dedup-Cache
X-Patu-Passthroughtrue, wenn keine Variante besser als die Eingabe war, sodass das Original unverändert zurückgegeben wird

Gespeicherte Antwort (Manifest)

{
  "id": "<64-hex content hash>",
  "url": "https://cdn.patu.dev/a/<id>",
  "original": { "width": 1024, "height": 683, "bytes": 240133, "format": "jpeg" },
  "variants": [
    { "format": "avif", "width": 1024, "height": 683, "bytes": 30215,
      "score": 81.5, "url": "https://cdn.patu.dev/a/<id>/1024.avif",
      "integrity": "sha384-…" }
  ],
  "created_at": "2026-06-29T12:00:00Z"
}

Die Wurzel-url ist eine content-negotiated URL: der Edge liefert das beste Format, das der Accept-Header der Anfrage erlaubt. Jede Varianten-url ist eine explizite, unveränderliche, inhaltsadressierte URL für genau ein Format und eine Breite. URLs sind absolut auf cdn.patu.dev, wenn das CDN aktiviert ist (Standard), oder Pod-relativ /v1/asset/<id>/…, wenn es deaktiviert ist. Erneutes Senden desselben Bilds mit neuen widths erweitert das Manifest nur um die neuen Breiten; eine identische Anfrage wird dedupliziert und liefert das bestehende Manifest unverändert zurück. Jede Variante trägt ihr integrity-Token, und der Edge liefert es als X-Patu-Integrity aus.

Nicht-Bild-Assets: JS, CSS, SVG und Schriften

Derselbe /v1/compress-Endpunkt (inline oder mode=stored) optimiert Text und Schriften. Sie haben keine Magic Bytes, also route sie über den Content-Type der Anfrage.

Content-TypeWas Patu macht
text/javascript, text/cssMinifiziert mit esbuild (nur Leerzeichen und sichere Syntax; Bezeichner werden niemals umbenannt, sodass exportierte und globale Namen erhalten bleiben). Der Edge komprimiert es bei der Auslieferung (Brotli/gzip).
font/ttf, font/otf, font/woff, font/collectionKonvertiert zu WOFF2. Optionales Glyph-Subsetting (siehe unten).
font/woff2Bereits das Zielformat; wird unverändert ausgeliefert.
image/svg+xmlBereinigt (Skripte, Event-Handler und externe Referenzen entfernt, gehärtete CSP bei der Auslieferung), dann mit silk pfad- und strukturoptimiert.

Die Ausgabe übersteigt nie die Eingabe. X-Patu-Format ist js, css, woff2 oder svg, und X-Patu-Score ist nicht anwendbar (-1). Gespeicherter Code und gespeicherte Schriften werden wie Bilder vom Edge ausgeliefert, mit demselben integrity-Wert.

Vergleiche bei JS und CSS X-Patu-Encoded-Bytes, nicht X-Patu-Output-Bytes. Minifizierung allein (Output-Bytes) bringt nur wenige Prozent; der eigentliche Gewinn ist Minifizierung plus das Brotli des Edge, was Encoded-Bytes meldet (die Größe, die über die Leitung geht): auf einem echten Korpus sind das etwa -90 % bei JS und -93 % bei CSS gegenüber dem Rohquelltext.

ParameterGilt fürBedeutung
subset-unicodesSchriftenSubset auf Code-Point-Bereiche, z. B. 0-FF,20AC,2000-206F. Entfernt ungenutzte Glyphen, der größte Gewinn bei Schriften. Hex, optionales U+-Präfix.
subset-textSchriftenSubset auf die Glyphen, die diesen String abdecken (vereinigt mit subset-unicodes). Gut für einen bekannten, kleinen Zeichensatz; für eine ganze Website die Code-Points zur Build-Zeit berechnen und subset-unicodes übergeben.
# minify a stylesheet
curl -X POST "https://patu.dev/v1/compress" \
  -H "X-Api-Key: $PATU_KEY" -H "Content-Type: text/css" \
  --data-binary @app.css -o app.min.css

# convert + subset a font to a Latin WOFF2
curl -X POST "https://patu.dev/v1/compress?subset-unicodes=0-FF,20AC" \
  -H "X-Api-Key: $PATU_KEY" -H "Content-Type: font/ttf" \
  --data-binary @Inter.ttf -o Inter.woff2

EPUB-Optimierung

POST /v1/epub optimiert einen gesamten EPUB-Container: eingebettete Bilder durchlaufen dieselbe Qualitätssuche wie /v1/compress, und TTF/OTF-Schriften werden auf die vom Buch tatsächlich genutzten Glyphen reduziert (Subsetting). Schicke die rohen .epub-Bytes als Request-Body.

ParameterStandardBedeutung
profilecompatOptimierungsprofil. compat ist das einzige von P1 akzeptierte Profil; jeder andere Wert ergibt einen 400.

Das Kontingent beträgt eine Einheit pro verarbeitetem Bild sowie eine pro Schrift mit Glyph-Subsetting, gezählt aus dem Manifest, bevor eine Kodierung läuft. Eine wegen Kontingent, Größe oder DRM abgelehnte Anfrage verbraucht nichts.

Synchron vs. asynchron

Bodies bis einschließlich 10 MB (10,485,760 Bytes) werden inline optimiert und direkt in der Antwort zurückgegeben. Größere Bodies werden in eine Warteschlange gestellt und mit 202 beantwortet:

{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/epub/a1b2c3d4e5f67890" }

Fragt GET /v1/epub/{id} ab: 202, solange der Job noch läuft, 200 mit dem optimierten .epub, sobald er fertig ist. Asynchrone Ergebnisse laufen 24 Stunden nach Abschluss ab. Ein Pod-Neustart verwirft jeden noch im Speicher wartenden Job; sein GET antwortet dann mit 404: sende die ursprüngliche Anfrage erneut.

Antwort-Header

X-Patu-Original-Bytes / X-Patu-Output-BytesEingabe- und Ausgabegröße des Containers
X-Patu-Images-ProcessedAnzahl tatsächlich neu kodierter Bilder
X-Patu-Fonts-SubsetAnzahl tatsächlich subsetteter Schriften
X-Patu-Quota-Useddas von dieser Anfrage tatsächlich verbrauchte Kontingent (verarbeitete Bilder + Schriften mit Subsetting)
X-Patu-Passthroughtrue, wenn das erneute Packen der optimierten Elemente keinen Gewinn brachte, sodass die ursprünglichen Bytes unverändert zurückgegeben werden

Maximale Upload-Größe nach Tarif

TarifMax. Upload
Free25 MB
Basic25 MB
Pro50 MB
Business150 MB

Fehler

400kein gültiger EPUB-Container, oder ein unbekanntes profile
413Upload über der maximalen Größe des Tarifs (siehe oben). Zählt nicht gegen das Kontingent.
422das Buch verwendet DRM-Verschlüsselung und kann nicht optimiert werden
429das monatliche Kontingent würde überschritten werden. Zählt nicht gegen das Kontingent.
# optimize a book (sync if <= 10 MB)
curl -X POST "https://patu.dev/v1/epub" \
  -H "X-Api-Key: $PATU_KEY" \
  --data-binary @book.epub -o book.optimized.epub

# a larger book: submit, then poll the async result
curl -X POST "https://patu.dev/v1/epub" -H "X-Api-Key: $PATU_KEY" \
  --data-binary @big-book.epub
curl "https://patu.dev/v1/epub/a1b2c3d4e5f67890" -H "X-Api-Key: $PATU_KEY" \
  -o big-book.optimized.epub

CBZ/CBR-Optimierung

POST /v1/cbz optimiert ein gesamtes Comic-Archiv: jede Seite durchläuft dieselbe formatgleiche Qualitätssuche wie /v1/compress (eine JPEG-Seite bleibt ein JPEG, eine PNG-Seite bleibt ein PNG), alles andere im Archiv - ComicInfo.xml, jede andere Begleitdatei - bleibt unangetastet, und Seitenreihenfolge und -namen ändern sich nie. Schicke die rohen .cbz- oder .cbr-Bytes als Request-Body; ein CBR-Archiv (RAR) wird transparent zu CBZ konvertiert.

ParameterStandardBedeutung
profilecompatOptimierungsprofil. compat ist das einzige akzeptierte Profil; jeder andere Wert ergibt einen 400.
max_widthnativVerkleinert jede Seite vor der Suche/Neukodierung auf höchstens diese Breite (nie hochskaliert). Ganzzahl, 0–8192; außerhalb des Bereichs ergibt einen 400.

Das Kontingent ist eine Einheit pro verarbeiteter Seite, gezählt aus dem Archiv, bevor eine Kodierung läuft. Eine wegen Kontingent oder Größe abgelehnte Anfrage verbraucht nichts.

Immer asynchron

Jede /v1/cbz-Anfrage wird in eine Warteschlange gestellt und mit 202 beantwortet - es gibt keinen synchronen Pfad, da selbst bei einem kleinen Comic-Archiv die vollständige Qualitätssuche über alle Seiten eine Weile dauern kann:

{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/cbz/a1b2c3d4e5f67890" }

Fragt GET /v1/cbz/{id} ab: 202, solange der Job noch läuft, 200 mit dem optimierten .cbz, sobald er fertig ist. Asynchrone Ergebnisse laufen 24 Stunden nach Abschluss ab. Ein Pod-Neustart verwirft jeden noch im Speicher wartenden Job; sein GET antwortet dann mit 404 - sende die ursprüngliche Anfrage erneut.

Antwort-Header (bei GET)

Content-Typeimmer application/vnd.comicbook+zip, unabhängig davon, ob die Eingabe CBZ oder CBR war
X-Patu-Container-Convertedcbz, nur vorhanden, wenn die Eingabe ein zu CBZ konvertiertes CBR-Archiv war

Fehler

400kein gültiger cbz/cbr-Container, oder ein unbekanntes profile/max_width
413Upload über der maximalen Größe des Tarifs, oder das Archiv überschreitet das Budget für die entpackte Größe. Zählt nicht gegen das Kontingent.
429das monatliche Kontingent würde überschritten werden. Zählt nicht gegen das Kontingent.
# submit a comic archive, then poll the async result
curl -X POST "https://patu.dev/v1/cbz" -H "X-Api-Key: $PATU_KEY" \
  --data-binary @issue-01.cbz
curl "https://patu.dev/v1/cbz/a1b2c3d4e5f67890" -H "X-Api-Key: $PATU_KEY" \
  -o issue-01.optimized.cbz

Office-Optimierung

POST /v1/office optimiert ein gesamtes Office-Dokument (.docx, .pptx, .odt, .odp, .ods): eingebettete Bilder durchlaufen dieselbe formatgleiche Qualitätssuche wie /v1/compress (ein JPEG-Bild bleibt ein JPEG, ein PNG bleibt ein PNG), alles andere - Markup, Stile, Beziehungen, eingebettete Objekte - bleibt unangetastet. Das Format des Dokuments ändert sich nie: es gibt keine Konvertierung, keine Größenänderung (kein max_width) und keine Wahl des profile. Schicke die rohen Dokument-Bytes als Request-Body. .xlsx (OOXML-Tabellenkalkulationen) wird namensbasiert abgelehnt - siehe Ablehnungen unten - .ods wird akzeptiert.

Das Kontingent ist eine Einheit pro verarbeitetem Bild, gezählt aus dem Dokument, bevor eine Kodierung läuft. Eine wegen Kontingent, Größe oder einer der unten genannten Ablehnungen abgelehnte Anfrage verbraucht nichts.

Synchron vs. asynchron

Bodies bis einschließlich 10 MB (10,485,760 Bytes) werden inline optimiert und direkt in der Antwort zurückgegeben. Größere Bodies werden in eine Warteschlange gestellt und mit 202 beantwortet:

{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/office/a1b2c3d4e5f67890" }

Fragt GET /v1/office/{id} ab: 202, solange der Job noch läuft, 200 mit dem optimierten Dokument, sobald er fertig ist. Asynchrone Ergebnisse laufen 24 Stunden nach Abschluss ab. Ein Pod-Neustart verwirft jeden noch im Speicher wartenden Job; sein GET antwortet dann mit 404 - sende die ursprüngliche Anfrage erneut.

Ablehnungen

Ein Dokument wird vollständig abgelehnt, bevor eine Kodierung läuft, wenn es einem der folgenden Fälle entspricht - jeder hat seine eigene 400-Meldung:

GrundDetail
Legacy-OLE-Formateine binäre .doc/.xls/.ppt-Datei von vor 2007 - erkannt an ihrer Compound-File-Signatur, nicht an der Dateiendung, daher wird sie abgelehnt, unabhängig davon, ob die Datei falsch benannt ist
Digital signiertdas Paket enthält einen Signaturteil (OOXMLs _xmlsignatures/ oder ODFs META-INF/documentsignatures.xml) - jedes erneute Komprimieren eines Eintrags würde die Signatur ungültig machen
.xlsx-Tabellenkalkulationwird anhand des Dateinamens abgelehnt, nicht des Inhalts: Tabellenkalkulationen enthalten praktisch keine eingebetteten Bilder, die eine Optimierung lohnen. Diese Prüfung benötigt den ursprünglichen Dateinamen des Clients, den POST /v1/office allein aus der Anfrage nicht ermitteln kann - über HTTP greift sie nie. Über die patu-CLI greift sie, da diese den echten Pfad kennt.

Fehler

400kein gültiges Office-Dokument, oder eine der oben genannten Ablehnungen
413Upload über der maximalen Größe des Tarifs, oder das Dokument überschreitet das Budget für die entpackte Größe. Zählt nicht gegen das Kontingent.
429das monatliche Kontingent würde überschritten werden. Zählt nicht gegen das Kontingent.
# optimize a document (sync if <= 10 MB)
curl -X POST "https://patu.dev/v1/office" \
  -H "X-Api-Key: $PATU_KEY" \
  --data-binary @report.docx -o report.optimized.docx

PDF-Optimierung

POST /v1/pdf optimiert ein ganzes PDF und gibt ein PDF zurück. Eingebettete JPEG-Bilder durchlaufen dieselbe Qualitätssuche wie /v1/compress (ein JPEG bleibt ein JPEG, ein Graustufenbild bleibt in Graustufen), und ineffizient codierte Streams (ASCII85, RunLength, LZW, schwaches Flate) werden verlustfrei neu komprimiert. Text, Schriften, Vektorgrafiken, Links und Formulare bleiben unberührt; JPEG-2000-, JBIG2- und CCITT-Bilder (typisch für Scans) bleiben, wie sie sind. Bevor das Ergebnis ausgeliefert wird, wird jede Seite gerendert und mit dem Original verglichen; weicht eine Seite ab oder ist nichts kleiner geworden, kommt das Originaldokument unverändert mit X-Patu-Passthrough: true zurück. target (ssimulacra2, Standard 85) legt das Qualitätsziel der Bilder fest. Ein an POST /v1/compress gesendetes PDF durchläuft dieselbe Verarbeitung, inline bis 10 MB, und kommt ebenfalls als PDF zurück.

Das Kontingent ist eine Einheit pro tatsächlich neu codiertem eingebettetem JPEG; der verlustfreie Stream-Durchgang ist kostenlos. Eine wegen Kontingent, Größe oder einer der folgenden Ablehnungen abgewiesene Anfrage verbraucht nichts.

Synchron vs. asynchron

Bodies bis einschließlich 10 MB (10.485.760 Bytes) werden direkt optimiert und in der Antwort zurückgegeben. Größere Bodies werden eingereiht und mit 202 beantwortet:

{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/pdf/a1b2c3d4e5f67890" }

Fragen Sie GET /v1/pdf/{id} ab: 202, solange der Job läuft, 200 mit dem optimierten PDF, sobald er fertig ist. Asynchrone Ergebnisse verfallen 24 Stunden nach Abschluss. Ein Pod-Neustart verwirft jeden noch im Speicher wartenden Job; dessen GET antwortet dann mit 404 - senden Sie die ursprüngliche Anfrage erneut.

Ablehnungen

Ein Dokument wird als Ganzes vor jeder Codierung abgelehnt, wenn einer dieser Fälle zutrifft - jeder hat seine eigene 400-Meldung:

GrundDetail
Verschlüsseltdas Neuschreiben würde die Verschlüsselung entfernen oder beschädigen
Digital signiertjede Änderung der Bytes macht die Signatur ungültig
PDF/Adie Archivkonformität würde das Neuschreiben ungeprüft nicht überstehen

Fehler

400kein lesbares PDF, eine Struktur, die Patu nicht sicher neu schreiben kann, oder eine der obigen Ablehnungen
413Upload über der maximalen Größe des Tarifs; bei /v1/compress ein PDF über 10 MB (nutzen Sie /v1/pdf). Zählt nicht zum Kontingent.
429das monatliche Kontingent würde überschritten. Zählt nicht zum Kontingent.
# optimize a PDF (sync if <= 10 MB)
curl -X POST "https://patu.dev/v1/pdf" \
  -H "X-Api-Key: $PATU_KEY" \
  --data-binary @invoice.pdf -o invoice.optimized.pdf

POST /v1/compress/batch

Komprimiert viele Bilder in einer Anfrage (nur im Stored-Modus). Schicke multipart/form-data mit einem Datei-Part pro Bild; widths, formats, target und async gelten für alle Parts.

{
  "results": [
    { "id": "…", "original": {…}, "variants": [{…}], "created_at": "…" },
    { "error": "image exceeds the pixel limit", "status": 413 }
  ]
}

Ein Slot pro Eingabebild, in Reihenfolge. Ein fehlerhaftes Bild lässt nur seinen Slot fehlschlagen, nicht den gesamten Batch. Mit async=1 ist die Antwort 202, und jeder Slot ist ein ausstehendes Manifest (seine URLs antworten mit 202, bis kodiert wurde). Jedes Bild zählt als eine Einheit des monatlichen Kontingents; der gesamte Batch zählt als eine Einheit des Rate-Limits. Grenzen: höchstens PATU_BATCH_MAX (20) Parts und eine begrenzte Gesamt-Payload-Größe pro Anfrage.

GET /v1/asset/{id}

Ruft ein gespeichertes Asset per Content-Negotiation ab. Die Antwort trägt ETag, Vary: Accept und einen cachefähigen Cache-Control.

FormLiefert
GET /v1/asset/{id}Die größte Breite, im besten Format, das der Accept-Header erlaubt (Präferenz AVIF > WebP > JPEG).
GET /v1/asset/{id}?w=768Die kleinste gespeicherte Breite ≥ 768 (oder die größte verfügbare, wenn keine passt), formatverhandelt.
GET /v1/asset/{id}/{width}.{ext}Genau eine Variante, z. B. /v1/asset/{id}/768.avif. Unveränderlich, hart gecacht.

Die Verhandlung folgt dem Accept-Header (RFC 9110, einschließlich q=0-Ausschlüssen): Accept: image/jpeg liefert JPEG, image/avif;q=0 überspringt AVIF, und */* oder kein Header liefert das beste Format (AVIF). Für deterministische Formatkontrolle unabhängig vom Client die expliziten {width}.{ext}-URLs verwenden (z. B. in <source type>-Tags).

Weitere Endpunkte

GET /v1/statsAuthentifiziert. Die aktuellen Qualitäts-Seeds des Learners je Format, erwartete Werte und Stichprobenzahlen.
GET /healthz, GET /readyzLiveness und Readiness. Keine Authentifizierung.
GET /metricsPrometheus-Metriken (Anfragen nach Klasse/Format/Status, Latenz-Histogramm, eingesparte Bytes).

Grenzwerte & Backpressure

Pro-Schlüssel-Limits verhindern, dass ein Tenant den Dienst überlastet. Die folgenden Werte gelten für den Free-Tarif; kostenpflichtige Tarife heben sie an. Beachte Retry-After, dann taktest du dich optimal.

LimitFree-TarifBei Überschreitung
Rate-Limit60 req/min pro Schlüssel429 + Retry-After
Tageskontingent25 req/day pro Schlüssel429 + Retry-After (Sekunden bis 00:00 UTC)
Monatskontingent750 req/month pro Schlüssel429 + Retry-After (Sekunden bis zum 1., 00:00 UTC)
Gleichzeitige Kodierungen4 gleichzeitig503 + Retry-After: 2, erneut versuchen, nicht als Fehler behandeln

Beide Kontingentfenster setzen sich um 00:00 UTC zurück (täglich jede Nacht, monatlich am 1.). Eine mit 429 oder 413 abgelehnte Anfrage zählt nicht gegen dein Kontingent. Ein 503 bedeutet, dass die Encoder momentan ausgelastet sind, nicht dass die Anfrage fehlerhaft war: warte Retry-After Sekunden ab und sende erneut. Cache-Treffer und erneute Uploads bereits gespeicherter Bilder werden vom Encoder nie ratenbegrenzt. widths wird nie über die Quellbreite hinaus hochskaliert, sodass eine Anfrage nach einer größeren Breite als dem Original stattdessen die Originalbreite liefert.

Fehler

401fehlender oder ungültiger X-Api-Key
400nicht unterstütztes oder beschädigtes Bild
413Payload über 25 MB (25,000,000 Bytes), Bild über 25,000,000 Pixel, oder ein animierter Clip jenseits des Frame-/Frame-Pixel-Limits. Zählt nicht gegen das Kontingent.
429Rate-, Tages- oder Monatskontingent überschritten, siehe Retry-After. Zählt nicht gegen das Kontingent.
503Encoder momentan ausgelastet, erneut versuchen nach Retry-After
501Stored-Modus angefordert, aber Storage ist nicht konfiguriert

Fehler-Bodies sind JSON: {"error": "..."}.