Referencia de la API de Patu
Cada solicitud se autentica con una cabecera X-Api-Key. La URL base es https://patu.dev. Envía los bytes en bruto del recurso como cuerpo de la solicitud. Patu optimiza imágenes, SVG, JavaScript, CSS y fuentes; los recursos que no son imágenes se enrutan por el Content-Type de la solicitud (consulta Recursos que no son imágenes). Los libros EPUB, los archivos de cómic CBZ/CBR y los documentos de Office se optimizan como un contenedor completo (consulta Optimización de EPUB, Optimización de CBZ/CBR, Optimización de Office, Optimización de PDF).
POST /v1/compress
Comprime una imagen. Devuelve los bytes (en línea, por defecto) o un manifiesto JSON (almacenado).
| Consulta | Por defecto | Significado |
|---|---|---|
mode | inline | inline devuelve los bytes de la variante elegida; stored conserva cada variante y devuelve un manifiesto. |
formats | avif,webp,fallback | Lista separada por comas. fallback resuelve a JPEG para fotos, WebP/PNG sin pérdida para gráficos o transparencia. |
widths | original | Lista separada por comas de anchos en píxeles. Nunca se aumenta la resolución más allá del ancho de origen. |
target | por clave | La puntuación ssimulacra2 a alcanzar (más alta = más cercana al original, archivo más grande). Típicamente 75–85. |
En modo en línea, el formato devuelto también respeta la cabecera Accept cuando no se define formats.
Respuesta en línea
Los bytes de la imagen, más cabeceras que describen el resultado:
X-Patu-Format | formato de salida elegido (avif/webp/jpeg, o js/css/woff2/svg) |
X-Patu-Original-Bytes / X-Patu-Output-Bytes | tamaños de entrada y salida |
X-Patu-Encoded-Bytes | para JS/CSS, el tamaño después de que el edge comprime la respuesta (Brotli/gzip) — lo que realmente cruza la red |
X-Patu-Latency-Ms | tiempo de procesamiento en el servidor para esta solicitud, en milisegundos |
X-Patu-Integrity | token de Subresource Integrity (sha384-…) de los bytes servidos, listo para <script>/<link> integrity="…" |
X-Patu-Score | puntuación ssimulacra2 (medida, o esperada en la vía rápida) |
X-Patu-Verified | true si la puntuación se midió en esta solicitud, false si fue predicha por el learner |
X-Patu-Cache | hit o miss en la caché de deduplicación del proceso |
X-Patu-Passthrough | true cuando ninguna variante supera a la entrada, así que el original se devuelve sin cambios |
Respuesta almacenada (manifiesto)
{
"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"
}
La url raíz es una URL con negociación de contenido: el edge devuelve el mejor formato que permite la cabecera Accept de la solicitud. Cada url de variante es una URL explícita, inmutable y direccionada por contenido para un formato y ancho exactos. Las URLs son absolutas en cdn.patu.dev cuando la CDN está activada (por defecto), o relativas al pod /v1/asset/<id>/… cuando está desactivada. Volver a enviar la misma imagen con nuevos widths extiende el manifiesto solo con los anchos nuevos; una solicitud idéntica se deduplica y devuelve el manifiesto existente sin cambios. Cada variante lleva su token integrity, y el edge lo sirve como X-Patu-Integrity.
Recursos que no son imágenes: JS, CSS, SVG y fuentes
El mismo endpoint /v1/compress (en línea o mode=stored) optimiza texto y fuentes. No tienen bytes mágicos, así que enrútalos con el Content-Type de la solicitud.
| Content-Type | Qué hace Patu |
|---|---|
text/javascript, text/css | Minifica con esbuild (solo espacios en blanco y sintaxis segura; los identificadores nunca se renombran, así que los nombres exportados y globales se conservan). El edge lo comprime (Brotli/gzip) al entregarlo. |
font/ttf, font/otf, font/woff, font/collection | Convierte a WOFF2. Subsetting de glifos opcional (abajo). |
font/woff2 | Ya es el formato de destino; se sirve sin cambios. |
image/svg+xml | Sanitizado (se eliminan scripts, manejadores de eventos y referencias externas, con una CSP reforzada en la entrega), y luego optimizado en path y estructura con silk. |
La salida nunca supera a la entrada. X-Patu-Format es js, css, woff2 o svg, y X-Patu-Score no aplica (-1). El código y las fuentes almacenados se sirven desde el edge igual que las imágenes, con la misma integrity.
Para JS y CSS, compara X-Patu-Encoded-Bytes, no X-Patu-Output-Bytes. La minificación por sí sola (Output-Bytes) es solo unos pocos puntos porcentuales; la verdadera ganancia es minificación más el Brotli del edge, que es lo que reporta Encoded-Bytes (el tamaño que cruza la red): en un corpus real eso es aproximadamente -90% en JS y -93% en CSS frente al código fuente sin procesar.
| Consulta | Aplica a | Significado |
|---|---|---|
subset-unicodes | fuentes | Subconjunto a rangos de puntos de código, p. ej. 0-FF,20AC,2000-206F. Elimina los glifos sin usar, la mayor ganancia en fuentes. Hexadecimal, prefijo U+ opcional. |
subset-text | fuentes | Subconjunto a los glifos que cubren esta cadena (unido con subset-unicodes). Bueno para un conjunto de caracteres pequeño y conocido; para un sitio completo, calcula los puntos de código en tiempo de compilación y pasa 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
Optimización de EPUB
POST /v1/epub optimiza un contenedor EPUB completo: las imágenes incrustadas pasan por la misma búsqueda de calidad que /v1/compress, y las fuentes TTF/OTF se subconjuntan a los glifos que el libro realmente usa. Envía los bytes en bruto del .epub como cuerpo de la solicitud.
| Consulta | Por defecto | Significado |
|---|---|---|
profile | compat | Perfil de optimización. compat es el único perfil que acepta P1; cualquier otro valor es un 400. |
La cuota es una unidad por imagen procesada más una por fuente subconjuntada, contada desde el manifiesto antes de que se ejecute cualquier codificación. Una solicitud rechazada por cuota, tamaño o DRM no consume nada.
Síncrono vs. asíncrono
Los cuerpos de 10 MB (10,485,760 bytes) o menos se optimizan en línea y se devuelven directamente en la respuesta. Los cuerpos más grandes se encolan y se responden con 202:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/epub/a1b2c3d4e5f67890" }
Consulta GET /v1/epub/{id}: 202 mientras el trabajo sigue en curso, 200 con el .epub optimizado cuando termina. Los resultados asíncronos caducan 24 horas después de completarse. Un reinicio del pod descarta cualquier trabajo aún pendiente en memoria; su GET responde entonces 404 — vuelve a enviar la solicitud original.
Cabeceras de respuesta
X-Patu-Original-Bytes / X-Patu-Output-Bytes | tamaños del contenedor de entrada y salida |
X-Patu-Images-Processed | número de imágenes realmente recodificadas |
X-Patu-Fonts-Subset | número de fuentes realmente subconjuntadas |
X-Patu-Quota-Used | la cuota que esta solicitud realmente consumió (imágenes procesadas + fuentes subconjuntadas) |
X-Patu-Passthrough | true cuando volver a comprimir los elementos optimizados no supuso ninguna ganancia, así que los bytes originales se devuelven sin cambios |
Tamaño máximo de subida por nivel
| Nivel | Subida máxima |
|---|---|
| Free | 25 MB |
| Basic | 25 MB |
| Pro | 50 MB |
| Business | 150 MB |
Errores
400 | no es un contenedor epub válido, o un profile desconocido |
413 | subida por encima del tamaño máximo del nivel (arriba). No cuenta contra la cuota. |
422 | el libro usa cifrado DRM y no se puede optimizar |
429 | se superaría la cuota mensual. No cuenta contra la cuota. |
# 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
Optimización de CBZ/CBR
POST /v1/cbz optimiza un archivo de cómic completo: cada página pasa por la misma búsqueda de calidad en el mismo formato que /v1/compress (una página JPEG sigue siendo JPEG, una página PNG sigue siendo PNG), todo lo demás en el archivo - ComicInfo.xml, cualquier otro archivo adicional - pasa sin tocarse, y el orden y los nombres de las páginas nunca cambian. Envía los bytes en bruto del .cbz o .cbr como cuerpo de la solicitud; un archivo CBR (RAR) se convierte de forma transparente a CBZ.
| Consulta | Por defecto | Significado |
|---|---|---|
profile | compat | Perfil de optimización. compat es el único perfil aceptado; cualquier otro valor es un 400. |
max_width | nativo | Redimensiona cada página como máximo a este ancho antes de la búsqueda/recodificación (nunca se aumenta la resolución). Entero, 0–8192; fuera de rango es un 400. |
La cuota es una unidad por página procesada, contada desde el archivo antes de que se ejecute cualquier codificación. Una solicitud rechazada por cuota o tamaño no consume nada.
Siempre asíncrono
Toda solicitud /v1/cbz se encola y se responde con 202 - no hay vía síncrona, ya que incluso la búsqueda de calidad de página completa de un archivo de cómic pequeño puede tardar un rato:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/cbz/a1b2c3d4e5f67890" }
Consulta GET /v1/cbz/{id}: 202 mientras el trabajo sigue en curso, 200 con el .cbz optimizado cuando termina. Los resultados asíncronos caducan 24 horas después de completarse. Un reinicio del pod descarta cualquier trabajo aún pendiente en memoria; su GET responde entonces 404 - vuelve a enviar la solicitud original.
Cabeceras de respuesta (en GET)
Content-Type | siempre application/vnd.comicbook+zip, sea la entrada CBZ o CBR |
X-Patu-Container-Converted | cbz, presente solo cuando la entrada era un archivo CBR convertido a CBZ |
Errores
400 | no es un contenedor cbz/cbr válido, o un profile/max_width desconocido |
413 | subida por encima del tamaño máximo del nivel, o el archivo supera el presupuesto de tamaño descomprimido. No cuenta contra la cuota. |
429 | se superaría la cuota mensual. No cuenta contra la cuota. |
# 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
Optimización de Office
POST /v1/office optimiza un documento de Office completo (.docx, .pptx, .odt, .odp, .ods): las imágenes incrustadas pasan por la misma búsqueda de calidad en el mismo formato que /v1/compress (una imagen JPEG sigue siendo JPEG, una PNG sigue siendo PNG), todo lo demás - marcado, estilos, relaciones, objetos incrustados - pasa sin tocarse. El formato del documento nunca cambia: no hay conversión, ni redimensionado (sin max_width) ni elección de profile. Envía los bytes en bruto del documento como cuerpo de la solicitud. .xlsx (hojas de cálculo OOXML) se rechaza por nombre - ver Rechazos abajo - .ods sí se acepta.
La cuota es una unidad por imagen procesada, contada desde el documento antes de que se ejecute cualquier codificación. Una solicitud rechazada por cuota, tamaño o uno de los rechazos siguientes no consume nada.
Síncrono vs. asíncrono
Los cuerpos de 10 MB (10,485,760 bytes) o menos se optimizan en línea y se devuelven directamente en la respuesta. Los cuerpos más grandes se encolan y se responden con 202:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/office/a1b2c3d4e5f67890" }
Consulta GET /v1/office/{id}: 202 mientras el trabajo sigue en curso, 200 con el documento optimizado cuando termina. Los resultados asíncronos caducan 24 horas después de completarse. Un reinicio del pod descarta cualquier trabajo aún pendiente en memoria; su GET responde entonces 404 - vuelve a enviar la solicitud original.
Rechazos
Un documento se rechaza por completo, antes de cualquier codificación, cuando coincide con uno de estos - cada uno tiene su propio mensaje 400 distinto:
| Motivo | Detalle |
|---|---|
| Formato OLE heredado | un binario anterior a 2007 .doc/.xls/.ppt - detectado por su firma de archivo compuesto, no por la extensión, así que se rechaza tenga o no un nombre incorrecto |
| Firmado digitalmente | el paquete contiene una parte de firma (_xmlsignatures/ de OOXML o META-INF/documentsignatures.xml de ODF) - recomprimir cualquier entrada invalidaría la firma |
| Hoja de cálculo .xlsx | se rechaza por nombre de archivo, no por contenido: las hojas de cálculo prácticamente no llevan imágenes incrustadas que valga la pena optimizar. Esta comprobación necesita el nombre de archivo original del cliente, que POST /v1/office no tiene forma de conocer solo a partir de la solicitud - nunca se activa por HTTP. Sí se activa desde la CLI patu, que conoce la ruta real. |
Errores
400 | no es un documento de office válido, o uno de los rechazos anteriores |
413 | subida por encima del tamaño máximo del nivel, o el documento supera el presupuesto de tamaño descomprimido. No cuenta contra la cuota. |
429 | se superaría la cuota mensual. No cuenta contra la cuota. |
# 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
Optimización de PDF
POST /v1/pdf optimiza un PDF completo y devuelve un PDF. Las imágenes JPEG incrustadas pasan por la misma búsqueda de calidad que /v1/compress (un JPEG sigue siendo un JPEG, una imagen en escala de grises sigue en escala de grises), y los flujos mal codificados (ASCII85, RunLength, LZW, Flate débil) se recomprimen sin pérdida. El texto, las fuentes, los dibujos vectoriales, los enlaces y los formularios no se tocan; las imágenes JPEG 2000, JBIG2 y CCITT (típicas de los escaneos) se dejan como están. Antes de enviar el resultado, cada página se renderiza y se compara con el original; si una página difiere, o si nada se redujo, el documento original vuelve sin cambios con X-Patu-Passthrough: true. target (ssimulacra2, 85 por defecto) fija el objetivo de calidad de las imágenes. Un PDF enviado a POST /v1/compress pasa por el mismo proceso, en línea hasta 10 MB, y también vuelve como PDF.
La cuota es una unidad por cada JPEG incrustado realmente recodificado; la pasada sin pérdida sobre los flujos es gratuita. Una petición rechazada por cuota, tamaño o uno de los rechazos siguientes no consume nada.
Síncrono vs. asíncrono
Los cuerpos de 10 MB o menos (10.485.760 bytes) se optimizan en línea y se devuelven directamente en la respuesta. Los cuerpos más grandes se encolan y se responden con 202:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/pdf/a1b2c3d4e5f67890" }
Consulte GET /v1/pdf/{id}: 202 mientras el trabajo sigue en curso, 200 con el PDF optimizado cuando termina. Los resultados asíncronos caducan 24 horas después de terminar. Un reinicio del pod descarta cualquier trabajo aún pendiente en memoria; su GET responde entonces 404 - reenvíe la petición original.
Rechazos
Un documento se rechaza entero, antes de cualquier codificación, cuando coincide con uno de estos casos; cada uno tiene su propio mensaje 400:
| Motivo | Detalle |
|---|---|
| Cifrado | reescribirlo eliminaría o rompería el cifrado |
| Firmado digitalmente | cualquier cambio en los bytes invalida la firma |
| PDF/A | la conformidad de archivo no sobreviviría a la reescritura sin control |
Errores
400 | PDF ilegible, una estructura que Patu no puede reescribir con seguridad, o uno de los rechazos anteriores |
413 | subida por encima del tamaño máximo del plan; en /v1/compress, un PDF de más de 10 MB (use /v1/pdf). No cuenta para la cuota. |
429 | se superaría la cuota mensual. No cuenta para la cuota. |
# 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
Comprime muchas imágenes en una sola solicitud (solo modo almacenado). Envía multipart/form-data con una parte de archivo por imagen; widths, formats, target y async se aplican a todas las partes.
{
"results": [
{ "id": "…", "original": {…}, "variants": [{…}], "created_at": "…" },
{ "error": "image exceeds the pixel limit", "status": 413 }
]
}
Un slot por imagen de entrada, en orden. Una imagen defectuosa solo falla en su slot, no en todo el lote. Con async=1 la respuesta es 202 y cada slot es un manifiesto pendiente (sus URLs responden 202 hasta que se codifican). Cada imagen cuenta como una unidad de cuota mensual; el lote completo es una unidad de límite de tasa. Límites: como máximo PATU_BATCH_MAX (20) partes y un tamaño total de payload acotado por solicitud.
GET /v1/asset/{id}
Obtén un recurso almacenado por negociación de contenido. La respuesta lleva ETag, Vary: Accept y un Cache-Control cacheable.
| Forma | Devuelve |
|---|---|
GET /v1/asset/{id} | El ancho más grande, en el mejor formato que permite la cabecera Accept (preferencia AVIF > WebP > JPEG). |
GET /v1/asset/{id}?w=768 | El ancho almacenado más pequeño ≥ 768 (o el más grande disponible si ninguno califica), negociado por formato. |
GET /v1/asset/{id}/{width}.{ext} | Una variante exacta, p. ej. /v1/asset/{id}/768.avif. Inmutable, con caché estricta. |
La negociación sigue la cabecera Accept (RFC 9110, incluidas las exclusiones q=0): Accept: image/jpeg devuelve JPEG, image/avif;q=0 omite AVIF, y */* o ninguna cabecera devuelve el mejor (AVIF). Para un control determinista del formato sin importar el cliente, usa las URLs explícitas {width}.{ext} (por ejemplo en etiquetas <source type>).
Otros endpoints
GET /v1/stats | Autenticado. Las semillas de calidad actuales por formato del learner, las puntuaciones esperadas y los recuentos de muestras. |
GET /healthz, GET /readyz | Liveness y readiness. Sin autenticación. |
GET /metrics | Métricas de Prometheus (recuentos de solicitudes por clase/formato/estado, histograma de latencia, bytes ahorrados). |
Límites y contrapresión
Los límites por clave evitan que un solo inquilino sature el servicio. Los valores de abajo son los del nivel Free; los niveles de pago los aumentan. Respeta Retry-After y te ajustarás al ritmo óptimo.
| Límite | Nivel Free | Al superarse |
|---|---|---|
| Tasa | 60 solicitudes/min por clave | 429 + Retry-After |
| Cuota diaria | 25 solicitudes/día por clave | 429 + Retry-After (segundos hasta las 00:00 UTC) |
| Cuota mensual | 750 solicitudes/mes por clave | 429 + Retry-After (segundos hasta el día 1, 00:00 UTC) |
| Codificaciones simultáneas | 4 en curso | 503 + Retry-After: 2, reintenta, no lo trates como fallo |
Ambas ventanas de cuota se reinician a las 00:00 UTC (la diaria cada noche, la mensual el día 1). Una solicitud rechazada con 429 o 413 no cuenta contra tu cuota. Un 503 significa que los codificadores están momentáneamente saturados, no que la solicitud estuviera mal: espera Retry-After segundos y vuelve a enviarla. Los aciertos de caché y los reenvíos de imágenes ya almacenadas nunca están limitados por el codificador. Los widths nunca aumentan la resolución más allá del ancho de origen, así que una solicitud de un ancho mayor que el original devuelve el ancho original en su lugar.
Errores
401 | X-Api-Key ausente o inválida |
400 | imagen no compatible o corrupta |
413 | payload por encima de 25 MB (25,000,000 bytes), imagen por encima de 25,000,000 píxeles, o un clip animado que supera el límite de fotogramas/píxeles por fotograma. No cuenta contra la cuota. |
429 | cuota de tasa, diaria o mensual superada, ver Retry-After. No cuenta contra la cuota. |
503 | codificadores momentáneamente saturados, reintenta después de Retry-After |
501 | se solicitó modo almacenado pero el almacenamiento no está configurado |
Los cuerpos de error son JSON: {"error": "..."}.