Patu

Référence de l'API Patu

Chaque requête est authentifiée avec un en-tête X-Api-Key. L'URL de base est https://patu.dev. Envoyez les octets bruts de la ressource comme corps de la requête. Patu optimise les images, le SVG, le JavaScript, le CSS et les polices ; les ressources non-image sont routées selon le Content-Type de la requête (voir Ressources non-image). Les livres EPUB, les archives de bande dessinée CBZ/CBR et les documents Office sont optimisés en tant que conteneur entier (voir Optimisation EPUB, Optimisation CBZ/CBR, Optimisation Office, Optimisation PDF).

POST /v1/compress

Compresse une image. Renvoie les octets (en ligne, par défaut) ou un manifeste JSON (stocké).

ParamètreDéfautSignification
modeinlineinline renvoie les octets de la variante choisie ; stored persiste chaque variante et renvoie un manifeste.
formatsavif,webp,fallbackListe séparée par des virgules. fallback se résout en JPEG pour les photos, en WebP/PNG sans perte pour les graphismes ou la transparence.
widthsoriginalListe de largeurs en pixels séparées par des virgules. Jamais agrandi au-delà de la largeur source.
targetpar cléLe score ssimulacra2 à viser (plus haut = plus proche de l'original, fichier plus lourd). Généralement 75–85.

En mode inline, le format renvoyé respecte aussi l'en-tête Accept quand formats n'est pas défini.

Réponse en ligne

Les octets de l'image, plus des en-têtes décrivant le résultat :

X-Patu-Formatformat final choisi (avif/webp/jpeg, ou js/css/woff2/svg)
X-Patu-Original-Bytes / X-Patu-Output-Bytestailles d'entrée et de sortie
X-Patu-Encoded-Bytespour JS/CSS, la taille après compression de la réponse en périphérie (Brotli/gzip), soit ce qui transite réellement sur le réseau
X-Patu-Latency-Mstemps de traitement côté serveur pour cette requête, en millisecondes
X-Patu-Integrityjeton Subresource Integrity (sha384-…) des octets servis, prêt pour <script>/<link> integrity="…"
X-Patu-Scorescore ssimulacra2 (mesuré, ou attendu sur le chemin rapide)
X-Patu-Verifiedtrue si le score a été mesuré pour cette requête, false s'il a été prédit par le module d'apprentissage
X-Patu-Cachehit ou miss sur le cache de déduplication interne au processus
X-Patu-Passthroughtrue quand aucune variante ne bat l'entrée, l'original est alors renvoyé inchangé

Réponse stockée (manifeste)

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

L'url racine est une URL à négociation de contenu : la périphérie renvoie le meilleur format que l'en-tête Accept de la requête autorise. Chaque url de variante est une URL explicite, immuable, adressée par contenu, pour un format et une largeur exacts. Les URLs sont absolues sur cdn.patu.dev quand le CDN est activé (par défaut), ou relatives au pod, /v1/asset/<id>/…, quand il ne l'est pas. Republier la même image avec de nouvelles widths étend le manifeste avec seulement les nouvelles largeurs ; une requête identique est dédupliquée et renvoie le manifeste existant inchangé. Chaque variante porte son jeton integrity, que la périphérie sert sous X-Patu-Integrity.

Ressources non-image : JS, CSS, SVG et polices

Le même endpoint /v1/compress (en ligne ou mode=stored) optimise le texte et les polices. Ils n'ont pas de magic bytes, donc routez-les avec le Content-Type de la requête.

Content-TypeCe que fait Patu
text/javascript, text/cssMinifié avec esbuild (espaces et syntaxe sûre uniquement ; les identifiants ne sont jamais renommés, les noms exportés et globaux sont donc conservés). La périphérie le compresse (Brotli/gzip) à la livraison.
font/ttf, font/otf, font/woff, font/collectionConverti en WOFF2. Sous-ensemble de glyphes en option (voir ci-dessous).
font/woff2Déjà le format cible ; servi inchangé.
image/svg+xmlNettoyé (scripts, gestionnaires d'événements et références externes supprimés, CSP renforcée à la livraison), puis chemins et structure optimisés avec silk.

La sortie ne dépasse jamais l'entrée. X-Patu-Format vaut js, css, woff2 ou svg, et X-Patu-Score ne s'applique pas (-1). Le code et les polices stockés sont servis depuis la périphérie comme les images, avec le même integrity.

Pour JS et CSS, comparez X-Patu-Encoded-Bytes, pas X-Patu-Output-Bytes. La minification seule (Output-Bytes) ne représente que quelques pour cent ; le vrai gain vient de la minification plus le Brotli de la périphérie, ce que rapporte Encoded-Bytes (la taille qui transite sur le réseau) : sur un corpus réel, cela représente environ -90% sur JS et -93% sur CSS par rapport à la source brute.

ParamètreS'applique àSignification
subset-unicodespolicesSous-ensemble selon des plages de points de code, p. ex. 0-FF,20AC,2000-206F. Supprime les glyphes inutilisés, le plus gros gain sur les polices. Hexadécimal, préfixe U+ optionnel.
subset-textpolicesSous-ensemble selon les glyphes couvrant cette chaîne (union avec subset-unicodes). Utile pour un petit jeu de caractères connu ; pour un site entier, calculez les points de code au moment du build et passez subset-unicodes.
# 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

Optimisation EPUB

POST /v1/epub optimise un conteneur EPUB entier : les images intégrées passent par la même recherche de qualité que /v1/compress, et les polices TTF/OTF sont réduites aux glyphes réellement utilisés par le livre. Envoyez les octets bruts .epub comme corps de la requête.

ParamètreDéfautSignification
profilecompatProfil d'optimisation. compat est le seul profil accepté par P1 ; toute autre valeur renvoie une 400.

Le quota est d'une unité par image traitée plus une par police réduite en sous-ensemble, compté à partir du manifeste avant tout encodage. Une requête rejetée pour quota, taille ou DRM ne consomme rien.

Synchrone vs asynchrone

Les corps de requête de 10 MB (10,485,760 octets) ou moins sont optimisés en ligne et renvoyés directement dans la réponse. Les corps plus volumineux sont mis en file d'attente et répondent avec 202 :

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

Interrogez GET /v1/epub/{id} : 202 tant que la tâche est en cours, 200 avec l'.epub optimisé une fois terminée. Les résultats asynchrones expirent 24 heures après la fin. Un redémarrage du pod supprime toute tâche encore en attente en mémoire ; son GET répond alors 404 : renvoyez la requête d'origine.

En-têtes de réponse

X-Patu-Original-Bytes / X-Patu-Output-Bytestailles du conteneur d'entrée et de sortie
X-Patu-Images-Processednombre d'images réellement réencodées
X-Patu-Fonts-Subsetnombre de polices réellement réduites en sous-ensemble
X-Patu-Quota-Usedle quota réellement consommé par cette requête (images traitées + polices réduites)
X-Patu-Passthroughtrue quand réarchiver les éléments optimisés n'a apporté aucun gain, les octets d'origine sont alors renvoyés inchangés

Taille maximale d'upload par palier

PalierUpload max
Free25 MB
Basic25 MB
Pro50 MB
Business150 MB

Erreurs

400conteneur epub invalide, ou profile inconnu
413upload dépassant la taille maximale du palier (ci-dessus). Ne compte pas dans le quota.
422le livre utilise un chiffrement DRM et ne peut pas être optimisé
429le quota mensuel serait dépassé. Ne compte pas dans le quota.
# 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

Optimisation CBZ/CBR

POST /v1/cbz optimise une archive de bande dessinée entière : chaque page passe par la même recherche de qualité à format identique que /v1/compress (une page JPEG reste en JPEG, une page PNG reste en PNG), tout le reste dans l'archive (ComicInfo.xml, tout autre fichier annexe) traverse sans modification, et l'ordre et les noms des pages ne changent jamais. Envoyez les octets bruts .cbz ou .cbr comme corps de la requête ; une archive CBR (RAR) est convertie de façon transparente en CBZ.

ParamètreDéfautSignification
profilecompatProfil d'optimisation. compat est le seul profil accepté ; toute autre valeur renvoie une 400.
max_widthnativeRedimensionne chaque page à au plus cette largeur avant la recherche/le réencodage (jamais agrandi). Entier, 0–8192 ; hors plage, c'est une 400.

Le quota est d'une unité par page traitée, compté à partir de l'archive avant tout encodage. Une requête rejetée pour quota ou taille ne consomme rien.

Toujours asynchrone

Chaque requête /v1/cbz est mise en file d'attente et répond avec 202 : il n'y a pas de chemin synchrone, car même la recherche de qualité complète d'une petite archive peut prendre du temps :

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

Interrogez GET /v1/cbz/{id} : 202 tant que la tâche est en cours, 200 avec le .cbz optimisé une fois terminée. Les résultats asynchrones expirent 24 heures après la fin. Un redémarrage du pod supprime toute tâche encore en attente en mémoire ; son GET répond alors 404 : renvoyez la requête d'origine.

En-têtes de réponse (sur GET)

Content-Typetoujours application/vnd.comicbook+zip, que l'entrée soit CBZ ou CBR
X-Patu-Container-Convertedcbz, présent seulement quand l'entrée était une archive CBR convertie en CBZ

Erreurs

400conteneur cbz/cbr invalide, ou profile/max_width inconnu
413upload dépassant la taille maximale du palier, ou archive dépassant le budget de taille décompressée. Ne compte pas dans le quota.
429le quota mensuel serait dépassé. Ne compte pas dans le quota.
# 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

Optimisation Office

POST /v1/office optimise un document Office entier (.docx, .pptx, .odt, .odp, .ods) : les images intégrées passent par la même recherche de qualité à format identique que /v1/compress (une image JPEG reste en JPEG, une PNG reste en PNG), tout le reste (balisage, styles, relations, objets intégrés) traverse sans modification. Le format du document ne change jamais : pas de conversion, pas de redimensionnement (pas de max_width) et pas de choix de profile. Envoyez les octets bruts du document comme corps de la requête. .xlsx (tableurs OOXML) est refusé par son nom (voir Refus ci-dessous), .ods est accepté.

Le quota est d'une unité par image traitée, compté à partir du document avant tout encodage. Une requête rejetée pour quota, taille, ou pour l'un des refus ci-dessous ne consomme rien.

Synchrone vs asynchrone

Les corps de requête de 10 MB (10,485,760 octets) ou moins sont optimisés en ligne et renvoyés directement dans la réponse. Les corps plus volumineux sont mis en file d'attente et répondent avec 202 :

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

Interrogez GET /v1/office/{id} : 202 tant que la tâche est en cours, 200 avec le document optimisé une fois terminée. Les résultats asynchrones expirent 24 heures après la fin. Un redémarrage du pod supprime toute tâche encore en attente en mémoire ; son GET répond alors 404 : renvoyez la requête d'origine.

Refus

Un document est refusé entièrement, avant tout encodage, quand il correspond à l'un de ces cas : chacun a son propre message 400 distinct.

RaisonDétail
Format OLE historiqueun binaire pré-2007 .doc/.xls/.ppt : détecté par sa signature de fichier composé, pas par l'extension, donc refusé que le fichier soit mal nommé ou non
Signé numériquementle paquet contient une partie signature (_xmlsignatures/ pour OOXML, ou META-INF/documentsignatures.xml pour ODF) : recompresser une entrée invaliderait la signature
Tableur .xlsxrefusé par le nom de fichier, pas par le contenu : les tableurs ne contiennent quasiment jamais d'images intégrées valant la peine d'être optimisées. Cette vérification a besoin du nom de fichier original du client, que POST /v1/office n'a aucun moyen de connaître à partir de la seule requête : elle ne se déclenche donc jamais en HTTP. Elle se déclenche depuis la CLI patu, qui connaît le vrai chemin.

Erreurs

400document office invalide, ou l'un des refus ci-dessus
413upload dépassant la taille maximale du palier, ou document dépassant le budget de taille décompressée. Ne compte pas dans le quota.
429le quota mensuel serait dépassé. Ne compte pas dans le quota.
# 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

Optimisation PDF

POST /v1/pdf optimise un PDF entier et renvoie un PDF. Les images JPEG intégrées passent par la même recherche de qualité que /v1/compress (un JPEG reste un JPEG, une image en niveaux de gris reste en niveaux de gris), et les flux mal encodés (ASCII85, RunLength, LZW, Flate faible) sont recompressés sans perte. Le texte, les polices, les dessins vectoriels, les liens et les formulaires ne sont pas touchés ; les images JPEG 2000, JBIG2 et CCITT (typiques des scans) restent telles quelles. Avant l'envoi du résultat, chaque page est rendue et comparée à l'original ; si une page diverge, ou si rien n'a diminué, le document original revient inchangé avec X-Patu-Passthrough: true. target (ssimulacra2, 85 par défaut) fixe l'objectif de qualité des images. Un PDF envoyé à POST /v1/compress suit le même traitement, en ligne jusqu'à 10 Mo, et revient lui aussi en PDF.

Le quota est d'une unité par JPEG intégré effectivement réencodé ; la passe sans perte sur les flux est gratuite. Une requête refusée pour quota, taille ou l'un des refus ci-dessous ne consomme rien.

Synchrone vs asynchrone

Les corps de 10 Mo ou moins (10 485 760 octets) sont optimisés directement et renvoyés dans la réponse. Les corps plus gros sont mis en file et reçoivent un 202 :

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

Interrogez GET /v1/pdf/{id} : 202 tant que la tâche tourne, 200 avec le PDF optimisé une fois terminée. Les résultats asynchrones expirent 24 heures après la fin. Un redémarrage du pod perd toute tâche encore en attente en mémoire ; son GET répond alors 404, renvoyez la requête d'origine.

Refus

Un document est refusé en entier, avant tout encodage, s'il correspond à l'un de ces cas ; chacun a son propre message 400 :

RaisonDétail
Chiffréla réécriture supprimerait ou casserait le chiffrement
Signé numériquementtoute modification des octets invalide la signature
PDF/Ala conformité d'archivage ne survivrait pas à la réécriture sans contrôle

Erreurs

400PDF illisible, structure que Patu ne peut pas réécrire sans risque, ou l'un des refus ci-dessus
413envoi au-delà de la taille maximale du forfait ; sur /v1/compress, un PDF de plus de 10 Mo (utilisez /v1/pdf). Ne compte pas dans le quota.
429le quota mensuel serait dépassé. Ne compte pas dans le quota.
# 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

Compresse plusieurs images en une seule requête (mode stocké uniquement). Envoyez du multipart/form-data avec une partie fichier par image ; widths, formats, target et async s'appliquent à toutes les parties.

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

Un emplacement par image d'entrée, dans l'ordre. Une image invalide ne fait échouer que son emplacement, pas le lot entier. Avec async=1, la réponse est 202 et chaque emplacement est un manifeste en attente (ses URLs répondent 202 jusqu'à l'encodage). Chaque image compte pour une unité de quota mensuel ; le lot entier compte pour une unité de limite de débit. Limites : au plus PATU_BATCH_MAX (20) parties, et une taille totale de charge utile bornée par requête.

GET /v1/asset/{id}

Récupère une ressource stockée par négociation de contenu. La réponse porte ETag, Vary: Accept et un Cache-Control cacheable.

FormeRenvoie
GET /v1/asset/{id}La plus grande largeur, dans le meilleur format autorisé par l'en-tête Accept (préférence AVIF > WebP > JPEG).
GET /v1/asset/{id}?w=768La plus petite largeur stockée ≥ 768 (ou la plus grande disponible si aucune ne convient), négociée par format.
GET /v1/asset/{id}/{width}.{ext}Une variante exacte, p. ex. /v1/asset/{id}/768.avif. Immuable, mise en cache de façon stricte.

La négociation suit l'en-tête Accept (RFC 9110, y compris les exclusions q=0) : Accept: image/jpeg renvoie du JPEG, image/avif;q=0 exclut l'AVIF, et */* ou l'absence d'en-tête renvoie le meilleur (AVIF). Pour un contrôle déterministe du format quel que soit le client, utilisez les URLs explicites {width}.{ext} (p. ex. dans des balises <source type>).

Autres endpoints

GET /v1/statsAuthentifié. Les graines de qualité actuelles par format du module d'apprentissage, les scores attendus et les tailles d'échantillon.
GET /healthz, GET /readyzLiveness et readiness. Sans authentification.
GET /metricsMétriques Prometheus (nombre de requêtes par classe/format/statut, histogramme de latence, octets économisés).

Limites et backpressure

Des limites par clé empêchent un locataire de saturer le service. Les valeurs ci-dessous sont celles du palier Free ; les paliers payants les relèvent. Respectez Retry-After pour vous cadencer de façon optimale.

LimitePalier FreeEn cas de dépassement
Débit60 req/min par clé429 + Retry-After
Quota quotidien25 req/jour par clé429 + Retry-After (secondes jusqu'à 00:00 UTC)
Quota mensuel750 req/mois par clé429 + Retry-After (secondes jusqu'au 1er, 00:00 UTC)
Encodages simultanés4 en cours503 + Retry-After: 2, réessayez, ne le traitez pas comme un échec

Les deux fenêtres de quota se réinitialisent à 00:00 UTC (chaque nuit pour le quotidien, le 1er pour le mensuel). Une requête rejetée avec 429 ou 413 ne compte pas dans votre quota. Une 503 signifie que les encodeurs sont momentanément saturés, pas que la requête était incorrecte : patientez Retry-After secondes puis renvoyez-la. Les hits de cache et les republications d'images déjà stockées ne sont jamais limités en débit par l'encodeur. Les widths ne sont jamais agrandies au-delà de la largeur source, donc une requête pour une largeur supérieure à l'original renvoie la largeur d'origine à la place.

Erreurs

401X-Api-Key manquante ou invalide
400image non prise en charge ou corrompue
413charge utile dépassant 25 MB (25,000,000 bytes), image dépassant 25,000,000 pixels, ou clip animé dépassant la limite de frames/pixels par frame. Ne compte pas dans le quota.
429quota de débit, quotidien ou mensuel dépassé, voir Retry-After. Ne compte pas dans le quota.
503encodeurs momentanément saturés, réessayez après Retry-After
501mode stocké demandé mais le stockage n'est pas configuré

Les corps d'erreur sont en JSON : {"error": "..."}.