Referência da API do Patu
Toda requisição é autenticada com um cabeçalho X-Api-Key. A URL base é https://patu.dev. Envie os bytes brutos do recurso como corpo da requisição. O Patu otimiza imagens, SVG, JavaScript, CSS e fontes; recursos que não são imagens são roteados pelo Content-Type da requisição (veja Recursos que não são imagens). Livros EPUB, arquivos de quadrinhos CBZ/CBR e documentos do Office são otimizados como um contêiner inteiro (veja Otimização de EPUB, Otimização de CBZ/CBR, Otimização do Office, Otimização de PDF).
POST /v1/compress
Comprime uma imagem. Retorna os bytes (em linha, padrão) ou um manifesto JSON (armazenado).
| Parâmetro | Padrão | Significado |
|---|---|---|
mode | inline | inline retorna os bytes da variante escolhida; stored persiste cada variante e retorna um manifesto. |
formats | avif,webp,fallback | Lista separada por vírgulas. fallback resolve para JPEG em fotos, WebP/PNG sem perdas para gráficos ou transparência. |
widths | original | Lista separada por vírgulas de larguras em pixels. Nunca aumentada além da largura original. |
target | por chave | A pontuação ssimulacra2 a ser buscada (quanto maior, mais próximo do original e maior o arquivo). Tipicamente 75–85. |
No modo em linha, o formato retornado também respeita o cabeçalho Accept quando formats não é definido.
Resposta em linha
Os bytes da imagem, mais cabeçalhos descrevendo o resultado:
X-Patu-Format | formato de transporte escolhido (avif/webp/jpeg, ou js/css/woff2/svg) |
X-Patu-Original-Bytes / X-Patu-Output-Bytes | tamanhos de entrada e saída |
X-Patu-Encoded-Bytes | para JS/CSS, o tamanho depois que a borda comprime a resposta (Brotli/gzip) — o que de fato trafega na rede |
X-Patu-Latency-Ms | tempo de processamento no servidor para esta requisição, em milissegundos |
X-Patu-Integrity | token de Subresource Integrity (sha384-…) dos bytes servidos, pronto para <script>/<link> integrity="…" |
X-Patu-Score | pontuação ssimulacra2 (medida, ou esperada no caminho rápido) |
X-Patu-Verified | true se a pontuação foi medida nesta requisição, false se prevista pelo learner |
X-Patu-Cache | hit ou miss no cache de deduplicação do processo |
X-Patu-Passthrough | true quando nenhuma variante superou a entrada, então o original é retornado sem alterações |
Resposta armazenada (manifesto)
{
"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"
}
A url raiz é uma URL única com negociação de conteúdo: a borda retorna o melhor formato que o Accept da requisição permitir. Cada url de variante é uma URL explícita, imutável e endereçada por conteúdo para um formato e largura exatos. As URLs são absolutas em cdn.patu.dev quando a CDN está habilitada (o padrão), ou relativas ao pod, /v1/asset/<id>/…, quando está desabilitada. Reenviar a mesma imagem com novas widths estende o manifesto apenas com as larguras novas; uma requisição idêntica é deduplicada e retorna o manifesto existente sem alterações. Cada variante carrega seu token integrity, e a borda o serve como X-Patu-Integrity.
Recursos que não são imagens: JS, CSS, SVG e fontes
O mesmo endpoint /v1/compress (em linha ou mode=stored) otimiza texto e fontes. Eles não têm magic bytes, então são roteados pelo Content-Type da requisição.
| Content-Type | O que o Patu faz |
|---|---|
text/javascript, text/css | Minifica com esbuild (apenas espaços em branco e sintaxe segura; identificadores nunca são renomeados, então nomes exportados e globais sobrevivem). A borda comprime (Brotli/gzip) na entrega. |
font/ttf, font/otf, font/woff, font/collection | Converte para WOFF2. Subsetting opcional de glifos (abaixo). |
font/woff2 | Já é o formato de destino; servido sem alterações. |
image/svg+xml | Sanitizado (scripts, manipuladores de eventos e referências externas removidos, CSP reforçada na entrega), depois otimizado em path e estrutura com o silk. |
A saída nunca excede a entrada. X-Patu-Format é js, css, woff2 ou svg, e X-Patu-Score não se aplica (-1). Código e fontes armazenados são servidos pela borda como imagens, com o mesmo integrity.
Para JS e CSS, compare X-Patu-Encoded-Bytes, não X-Patu-Output-Bytes. A minificação isolada (Output-Bytes) é de apenas alguns por cento; o ganho real é a minificação somada ao Brotli da borda, que é o que Encoded-Bytes reporta (o tamanho que trafega na rede): em um corpus real isso é de aproximadamente -90% em JS e -93% em CSS em relação à fonte bruta.
| Parâmetro | Aplica-se a | Significado |
|---|---|---|
subset-unicodes | fontes | Subconjunto para intervalos de pontos de código, ex.: 0-FF,20AC,2000-206F. Remove glifos não usados, o maior ganho de tamanho em fontes. Hexadecimal, prefixo U+ opcional. |
subset-text | fontes | Subconjunto para os glifos que cobrem esta string (combinado com subset-unicodes). Bom para um conjunto de caracteres pequeno e conhecido; para um site inteiro, calcule os pontos de código no momento do build e passe 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
Otimização de EPUB
POST /v1/epub otimiza um contêiner EPUB inteiro: as imagens embutidas passam pela mesma busca de qualidade do /v1/compress, e as fontes TTF/OTF passam por subsetting para os glifos que o livro realmente usa. Envie os bytes brutos do .epub como corpo da requisição.
| Parâmetro | Padrão | Significado |
|---|---|---|
profile | compat | Perfil de otimização. compat é o único perfil que o P1 aceita; qualquer outro valor resulta em 400. |
A cota é de uma unidade por imagem processada mais uma por fonte submetida a subsetting, contada a partir do manifesto antes de qualquer codificação rodar. Uma requisição rejeitada por cota, tamanho ou DRM não consome nada.
Síncrono vs. assíncrono
Corpos com até 10 MB (10,485,760 bytes) são otimizados em linha e retornados diretamente na resposta. Corpos maiores são enfileirados e respondidos com 202:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/epub/a1b2c3d4e5f67890" }
Consulte GET /v1/epub/{id}: 202 enquanto o job ainda está em execução, 200 com o .epub otimizado quando terminar. Resultados assíncronos expiram 24 horas após a conclusão. Um restart do pod descarta qualquer job ainda pendente em memória; seu GET então responde 404 — reenvie a requisição original.
Cabeçalhos de resposta
X-Patu-Original-Bytes / X-Patu-Output-Bytes | tamanhos do contêiner de entrada e saída |
X-Patu-Images-Processed | número de imagens de fato recodificadas |
X-Patu-Fonts-Subset | número de fontes de fato submetidas a subsetting |
X-Patu-Quota-Used | a cota que esta requisição de fato consumiu (imagens processadas + fontes com subsetting) |
X-Patu-Passthrough | true quando reempacotar os itens otimizados não gerou ganho, então os bytes originais são retornados sem alterações |
Tamanho máximo de upload por plano
| Plano | Upload máximo |
|---|---|
| Free | 25 MB |
| Basic | 25 MB |
| Pro | 50 MB |
| Business | 150 MB |
Erros
400 | contêiner epub inválido, ou um profile desconhecido |
413 | upload acima do tamanho máximo do plano (acima). Não conta na cota. |
422 | o livro usa criptografia DRM e não pode ser otimizado |
429 | a cota mensal seria excedida. Não conta na cota. |
# 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
Otimização de CBZ/CBR
POST /v1/cbz otimiza um arquivo de quadrinhos inteiro: cada página passa pela mesma busca de qualidade no mesmo formato do /v1/compress (uma página JPEG continua JPEG, uma página PNG continua PNG), tudo o mais no arquivo - ComicInfo.xml, qualquer outro arquivo complementar - passa intacto, e a ordem e os nomes das páginas nunca mudam. Envie os bytes brutos do .cbz ou .cbr como corpo da requisição; um arquivo CBR (RAR) é convertido de forma transparente para CBZ.
| Parâmetro | Padrão | Significado |
|---|---|---|
profile | compat | Perfil de otimização. compat é o único perfil aceito; qualquer outro valor resulta em 400. |
max_width | nativa | Redimensiona cada página para no máximo esta largura antes da busca/recodificação (nunca aumentada). Inteiro, 0–8192; fora do intervalo resulta em 400. |
A cota é de uma unidade por página processada, contada a partir do arquivo antes de qualquer codificação rodar. Uma requisição rejeitada por cota ou tamanho não consome nada.
Sempre assíncrono
Toda requisição a /v1/cbz é enfileirada e respondida com 202 - não há caminho síncrono, já que mesmo a busca de qualidade em todas as páginas de um arquivo de quadrinhos pequeno pode demorar um pouco:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/cbz/a1b2c3d4e5f67890" }
Consulte GET /v1/cbz/{id}: 202 enquanto o job ainda está em execução, 200 com o .cbz otimizado quando terminar. Resultados assíncronos expiram 24 horas após a conclusão. Um restart do pod descarta qualquer job ainda pendente em memória; seu GET então responde 404 - reenvie a requisição original.
Cabeçalhos de resposta (no GET)
Content-Type | sempre application/vnd.comicbook+zip, seja a entrada CBZ ou CBR |
X-Patu-Container-Converted | cbz, presente apenas quando a entrada era um arquivo CBR convertido para CBZ |
Erros
400 | contêiner cbz/cbr inválido, ou um profile/max_width desconhecido |
413 | upload acima do tamanho máximo do plano, ou o arquivo excede o orçamento de tamanho descomprimido. Não conta na cota. |
429 | a cota mensal seria excedida. Não conta na cota. |
# 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
Otimização do Office
POST /v1/office otimiza um documento do Office inteiro (.docx, .pptx, .odt, .odp, .ods): as imagens embutidas passam pela mesma busca de qualidade no mesmo formato do /v1/compress (uma imagem JPEG continua JPEG, uma PNG continua PNG), tudo o mais - marcação, estilos, relacionamentos, objetos embutidos - passa intacto. O formato do documento nunca muda: não há conversão, nem redimensionamento (nenhum max_width) e nenhuma escolha de profile. Envie os bytes brutos do documento como corpo da requisição. .xlsx (planilhas OOXML) é recusado pelo nome - veja Recusas abaixo - .ods é aceito.
A cota é de uma unidade por imagem processada, contada a partir do documento antes de qualquer codificação rodar. Uma requisição rejeitada por cota, tamanho ou uma das recusas abaixo não consome nada.
Síncrono vs. assíncrono
Corpos com até 10 MB (10,485,760 bytes) são otimizados em linha e retornados diretamente na resposta. Corpos maiores são enfileirados e respondidos com 202:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/office/a1b2c3d4e5f67890" }
Consulte GET /v1/office/{id}: 202 enquanto o job ainda está em execução, 200 com o documento otimizado quando terminar. Resultados assíncronos expiram 24 horas após a conclusão. Um restart do pod descarta qualquer job ainda pendente em memória; seu GET então responde 404 - reenvie a requisição original.
Recusas
Um documento é recusado por completo, antes de qualquer codificação, quando corresponde a um destes - cada um recebe sua própria mensagem 400 distinta:
| Motivo | Detalhe |
|---|---|
| Formato OLE legado | um .doc/.xls/.ppt binário pré-2007 - detectado pela sua assinatura de compound-file, não pela extensão, então é recusado mesmo que o arquivo tenha o nome trocado |
| Assinado digitalmente | o pacote contém uma parte de assinatura (_xmlsignatures/ do OOXML ou META-INF/documentsignatures.xml do ODF) - recomprimir qualquer entrada invalidaria a assinatura |
| planilha .xlsx | recusada pelo nome do arquivo, não pelo conteúdo: planilhas praticamente não carregam imagens embutidas que valham a pena otimizar. Essa verificação precisa do nome de arquivo original do cliente, que POST /v1/office não tem como descobrir a partir da requisição sozinha - ela nunca dispara via HTTP. Ela dispara a partir da CLI patu, que conhece o caminho real. |
Erros
400 | documento office inválido, ou uma das recusas acima |
413 | upload acima do tamanho máximo do plano, ou o documento excede o orçamento de tamanho descomprimido. Não conta na cota. |
429 | a cota mensal seria excedida. Não conta na cota. |
# 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
Otimização de PDF
POST /v1/pdf otimiza um PDF inteiro e devolve um PDF. As imagens JPEG incorporadas passam pela mesma busca de qualidade que /v1/compress (um JPEG continua JPEG, uma imagem em tons de cinza continua em tons de cinza), e os fluxos mal codificados (ASCII85, RunLength, LZW, Flate fraco) são recomprimidos sem perda. Texto, fontes, desenhos vetoriais, links e formulários não são tocados; as imagens JPEG 2000, JBIG2 e CCITT (típicas de digitalizações) ficam como estão. Antes de o resultado ser enviado, cada página é renderizada e comparada com o original; se uma página divergir, ou se nada ficou menor, o documento original volta inalterado com X-Patu-Passthrough: true. target (ssimulacra2, padrão 85) define o objetivo de qualidade das imagens. Um PDF enviado para POST /v1/compress passa pelo mesmo processo, em linha até 10 MB, e também volta como PDF.
A cota é de uma unidade por JPEG incorporado efetivamente recodificado; a passagem sem perda nos fluxos é gratuita. Uma requisição recusada por cota, tamanho ou uma das recusas abaixo não consome nada.
Síncrono vs. assíncrono
Corpos de até 10 MB (10.485.760 bytes) são otimizados em linha e devolvidos diretamente na resposta. Corpos maiores são enfileirados e respondidos com 202:
{ "id": "a1b2c3d4e5f67890", "status_url": "/v1/pdf/a1b2c3d4e5f67890" }
Consulte GET /v1/pdf/{id}: 202 enquanto o trabalho estiver em andamento, 200 com o PDF otimizado quando terminar. Os resultados assíncronos expiram 24 horas após a conclusão. Um reinício do pod descarta qualquer trabalho ainda pendente em memória; o seu GET responde então 404 - reenvie a requisição original.
Recusas
Um documento é recusado por inteiro, antes de qualquer codificação, quando corresponde a um destes casos; cada um tem a sua própria mensagem 400:
| Motivo | Detalhe |
|---|---|
| Criptografado | reescrevê-lo removeria ou quebraria a criptografia |
| Assinado digitalmente | qualquer alteração nos bytes invalida a assinatura |
| PDF/A | a conformidade de arquivamento não sobreviveria à reescrita sem verificação |
Erros
400 | PDF ilegível, uma estrutura que o Patu não consegue reescrever com segurança, ou uma das recusas acima |
413 | envio acima do tamanho máximo do plano; em /v1/compress, um PDF acima de 10 MB (use /v1/pdf). Não conta na cota. |
429 | a cota mensal seria excedida. Não conta na cota. |
# 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 várias imagens em uma requisição (apenas modo armazenado). Envie multipart/form-data com uma parte de arquivo por imagem; widths, formats, target e async se aplicam a todas as partes.
{
"results": [
{ "id": "…", "original": {…}, "variants": [{…}], "created_at": "…" },
{ "error": "image exceeds the pixel limit", "status": 413 }
]
}
Um slot por imagem de entrada, em ordem. Uma imagem ruim falha apenas em seu slot, não no lote inteiro. Com async=1 a resposta é 202 e cada slot é um manifesto pendente (suas URLs respondem 202 até serem codificadas). Cada imagem conta como uma unidade de cota mensal; o lote inteiro é uma unidade de rate limit. Limites: no máximo PATU_BATCH_MAX (20) partes e um tamanho total de payload limitado por requisição.
GET /v1/asset/{id}
Busca um recurso armazenado por negociação de conteúdo. A resposta carrega ETag, Vary: Accept e um Cache-Control cacheável.
| Forma | Retorna |
|---|---|
GET /v1/asset/{id} | A maior largura, no melhor formato que o cabeçalho Accept permitir (preferência AVIF > WebP > JPEG). |
GET /v1/asset/{id}?w=768 | A menor largura armazenada ≥ 768 (ou a maior disponível se nenhuma se qualificar), negociada por formato. |
GET /v1/asset/{id}/{width}.{ext} | Uma variante exata, ex. /v1/asset/{id}/768.avif. Imutável, com cache definitivo. |
A negociação segue o cabeçalho Accept (RFC 9110, incluindo exclusões q=0): Accept: image/jpeg retorna JPEG, image/avif;q=0 pula o AVIF, e */* ou nenhum cabeçalho retorna o melhor (AVIF). Para controle determinístico do formato independente do cliente, use as URLs explícitas {width}.{ext} (ex. em tags <source type>).
Outros endpoints
GET /v1/stats | Autenticado. As seeds de qualidade por formato atuais do learner, pontuações esperadas e contagens de amostras. |
GET /healthz, GET /readyz | Liveness e readiness. Sem autenticação. |
GET /metrics | Métricas Prometheus (contagem de requisições por classe/formato/status, histograma de latência, bytes economizados). |
Limites e backpressure
Limites por chave impedem que um único tenant sature o serviço. Os valores abaixo são os do plano Free; planos pagos os aumentam. Respeite o Retry-After e você vai se ritmar de forma ideal.
| Limite | Plano Free | Ao exceder |
|---|---|---|
| Taxa | 60 req/min por chave | 429 + Retry-After |
| Cota diária | 25 req/dia por chave | 429 + Retry-After (segundos até 00:00 UTC) |
| Cota mensal | 750 req/mês por chave | 429 + Retry-After (segundos até o dia 1º, 00:00 UTC) |
| Codificações simultâneas | 4 em andamento | 503 + Retry-After: 2, tente novamente, não trate como falha |
As duas janelas de cota reiniciam à 00:00 UTC (diária toda noite, mensal no dia 1º). Uma requisição rejeitada com 429 ou 413 não conta na sua cota. Um 503 significa que os codificadores estão momentaneamente cheios, não que a requisição estava errada: aguarde Retry-After segundos e reenvie. Cache hits e reenvios de imagens já armazenadas nunca são limitados por taxa pelo codificador. As widths nunca são aumentadas além da largura original, então uma requisição para uma largura maior que a original retorna a largura original.
Erros
401 | X-Api-Key ausente ou inválida |
400 | imagem não suportada ou corrompida |
413 | payload acima de 25 MB (25,000,000 bytes), imagem acima de 25,000,000 pixels, ou um clipe animado além do limite de frames/pixels por frame. Não conta na cota. |
429 | cota de taxa, diária ou mensal excedida, veja Retry-After. Não conta na cota. |
503 | codificadores momentaneamente cheios, tente novamente após Retry-After |
501 | modo armazenado solicitado, mas o armazenamento não está configurado |
Corpos de erro são JSON: {"error": "..."}.