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ètre | Défaut | Signification |
|---|---|---|
mode | inline | inline renvoie les octets de la variante choisie ; stored persiste chaque variante et renvoie un manifeste. |
formats | avif,webp,fallback | Liste 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. |
widths | original | Liste de largeurs en pixels séparées par des virgules. Jamais agrandi au-delà de la largeur source. |
target | par 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-Format | format final choisi (avif/webp/jpeg, ou js/css/woff2/svg) |
X-Patu-Original-Bytes / X-Patu-Output-Bytes | tailles d'entrée et de sortie |
X-Patu-Encoded-Bytes | pour 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-Ms | temps de traitement côté serveur pour cette requête, en millisecondes |
X-Patu-Integrity | jeton Subresource Integrity (sha384-…) des octets servis, prêt pour <script>/<link> integrity="…" |
X-Patu-Score | score ssimulacra2 (mesuré, ou attendu sur le chemin rapide) |
X-Patu-Verified | true si le score a été mesuré pour cette requête, false s'il a été prédit par le module d'apprentissage |
X-Patu-Cache | hit ou miss sur le cache de déduplication interne au processus |
X-Patu-Passthrough | true 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-Type | Ce que fait Patu |
|---|---|
text/javascript, text/css | Minifié 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/collection | Converti en WOFF2. Sous-ensemble de glyphes en option (voir ci-dessous). |
font/woff2 | Déjà le format cible ; servi inchangé. |
image/svg+xml | Nettoyé (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ètre | S'applique à | Signification |
|---|---|---|
subset-unicodes | polices | Sous-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-text | polices | Sous-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ètre | Défaut | Signification |
|---|---|---|
profile | compat | Profil 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-Bytes | tailles du conteneur d'entrée et de sortie |
X-Patu-Images-Processed | nombre d'images réellement réencodées |
X-Patu-Fonts-Subset | nombre de polices réellement réduites en sous-ensemble |
X-Patu-Quota-Used | le quota réellement consommé par cette requête (images traitées + polices réduites) |
X-Patu-Passthrough | true 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
| Palier | Upload max |
|---|---|
| Free | 25 MB |
| Basic | 25 MB |
| Pro | 50 MB |
| Business | 150 MB |
Erreurs
400 | conteneur epub invalide, ou profile inconnu |
413 | upload dépassant la taille maximale du palier (ci-dessus). Ne compte pas dans le quota. |
422 | le livre utilise un chiffrement DRM et ne peut pas être optimisé |
429 | le 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ètre | Défaut | Signification |
|---|---|---|
profile | compat | Profil d'optimisation. compat est le seul profil accepté ; toute autre valeur renvoie une 400. |
max_width | native | Redimensionne 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-Type | toujours application/vnd.comicbook+zip, que l'entrée soit CBZ ou CBR |
X-Patu-Container-Converted | cbz, présent seulement quand l'entrée était une archive CBR convertie en CBZ |
Erreurs
400 | conteneur cbz/cbr invalide, ou profile/max_width inconnu |
413 | upload dépassant la taille maximale du palier, ou archive dépassant le budget de taille décompressée. Ne compte pas dans le quota. |
429 | le 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.
| Raison | Détail |
|---|---|
| Format OLE historique | un 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ériquement | le paquet contient une partie signature (_xmlsignatures/ pour OOXML, ou META-INF/documentsignatures.xml pour ODF) : recompresser une entrée invaliderait la signature |
| Tableur .xlsx | refusé 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
400 | document office invalide, ou l'un des refus ci-dessus |
413 | upload dépassant la taille maximale du palier, ou document dépassant le budget de taille décompressée. Ne compte pas dans le quota. |
429 | le 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 :
| Raison | Détail |
|---|---|
| Chiffré | la réécriture supprimerait ou casserait le chiffrement |
| Signé numériquement | toute modification des octets invalide la signature |
| PDF/A | la conformité d'archivage ne survivrait pas à la réécriture sans contrôle |
Erreurs
400 | PDF illisible, structure que Patu ne peut pas réécrire sans risque, ou l'un des refus ci-dessus |
413 | envoi 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. |
429 | le 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.
| Forme | Renvoie |
|---|---|
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=768 | La 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/stats | Authentifié. Les graines de qualité actuelles par format du module d'apprentissage, les scores attendus et les tailles d'échantillon. |
GET /healthz, GET /readyz | Liveness et readiness. Sans authentification. |
GET /metrics | Mé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.
| Limite | Palier Free | En cas de dépassement |
|---|---|---|
| Débit | 60 req/min par clé | 429 + Retry-After |
| Quota quotidien | 25 req/jour par clé | 429 + Retry-After (secondes jusqu'à 00:00 UTC) |
| Quota mensuel | 750 req/mois par clé | 429 + Retry-After (secondes jusqu'au 1er, 00:00 UTC) |
| Encodages simultanés | 4 en cours | 503 + 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
401 | X-Api-Key manquante ou invalide |
400 | image non prise en charge ou corrompue |
413 | charge 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. |
429 | quota de débit, quotidien ou mensuel dépassé, voir Retry-After. Ne compte pas dans le quota. |
503 | encodeurs momentanément saturés, réessayez après Retry-After |
501 | mode stocké demandé mais le stockage n'est pas configuré |
Les corps d'erreur sont en JSON : {"error": "..."}.