Patu

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âmetroPadrãoSignificado
modeinlineinline retorna os bytes da variante escolhida; stored persiste cada variante e retorna um manifesto.
formatsavif,webp,fallbackLista separada por vírgulas. fallback resolve para JPEG em fotos, WebP/PNG sem perdas para gráficos ou transparência.
widthsoriginalLista separada por vírgulas de larguras em pixels. Nunca aumentada além da largura original.
targetpor chaveA 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-Formatformato de transporte escolhido (avif/webp/jpeg, ou js/css/woff2/svg)
X-Patu-Original-Bytes / X-Patu-Output-Bytestamanhos de entrada e saída
X-Patu-Encoded-Bytespara JS/CSS, o tamanho depois que a borda comprime a resposta (Brotli/gzip) — o que de fato trafega na rede
X-Patu-Latency-Mstempo de processamento no servidor para esta requisição, em milissegundos
X-Patu-Integritytoken de Subresource Integrity (sha384-…) dos bytes servidos, pronto para <script>/<link> integrity="…"
X-Patu-Scorepontuação ssimulacra2 (medida, ou esperada no caminho rápido)
X-Patu-Verifiedtrue se a pontuação foi medida nesta requisição, false se prevista pelo learner
X-Patu-Cachehit ou miss no cache de deduplicação do processo
X-Patu-Passthroughtrue 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-TypeO que o Patu faz
text/javascript, text/cssMinifica 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/collectionConverte para WOFF2. Subsetting opcional de glifos (abaixo).
font/woff2Já é o formato de destino; servido sem alterações.
image/svg+xmlSanitizado (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âmetroAplica-se aSignificado
subset-unicodesfontesSubconjunto 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-textfontesSubconjunto 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âmetroPadrãoSignificado
profilecompatPerfil 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-Bytestamanhos do contêiner de entrada e saída
X-Patu-Images-Processednúmero de imagens de fato recodificadas
X-Patu-Fonts-Subsetnúmero de fontes de fato submetidas a subsetting
X-Patu-Quota-Useda cota que esta requisição de fato consumiu (imagens processadas + fontes com subsetting)
X-Patu-Passthroughtrue 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

PlanoUpload máximo
Free25 MB
Basic25 MB
Pro50 MB
Business150 MB

Erros

400contêiner epub inválido, ou um profile desconhecido
413upload acima do tamanho máximo do plano (acima). Não conta na cota.
422o livro usa criptografia DRM e não pode ser otimizado
429a 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âmetroPadrãoSignificado
profilecompatPerfil de otimização. compat é o único perfil aceito; qualquer outro valor resulta em 400.
max_widthnativaRedimensiona 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-Typesempre application/vnd.comicbook+zip, seja a entrada CBZ ou CBR
X-Patu-Container-Convertedcbz, presente apenas quando a entrada era um arquivo CBR convertido para CBZ

Erros

400contêiner cbz/cbr inválido, ou um profile/max_width desconhecido
413upload acima do tamanho máximo do plano, ou o arquivo excede o orçamento de tamanho descomprimido. Não conta na cota.
429a 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:

MotivoDetalhe
Formato OLE legadoum .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 digitalmenteo pacote contém uma parte de assinatura (_xmlsignatures/ do OOXML ou META-INF/documentsignatures.xml do ODF) - recomprimir qualquer entrada invalidaria a assinatura
planilha .xlsxrecusada 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

400documento office inválido, ou uma das recusas acima
413upload acima do tamanho máximo do plano, ou o documento excede o orçamento de tamanho descomprimido. Não conta na cota.
429a 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:

MotivoDetalhe
Criptografadoreescrevê-lo removeria ou quebraria a criptografia
Assinado digitalmentequalquer alteração nos bytes invalida a assinatura
PDF/Aa conformidade de arquivamento não sobreviveria à reescrita sem verificação

Erros

400PDF ilegível, uma estrutura que o Patu não consegue reescrever com segurança, ou uma das recusas acima
413envio acima do tamanho máximo do plano; em /v1/compress, um PDF acima de 10 MB (use /v1/pdf). Não conta na cota.
429a 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.

FormaRetorna
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=768A 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/statsAutenticado. As seeds de qualidade por formato atuais do learner, pontuações esperadas e contagens de amostras.
GET /healthz, GET /readyzLiveness e readiness. Sem autenticação.
GET /metricsMé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.

LimitePlano FreeAo exceder
Taxa60 req/min por chave429 + Retry-After
Cota diária25 req/dia por chave429 + Retry-After (segundos até 00:00 UTC)
Cota mensal750 req/mês por chave429 + Retry-After (segundos até o dia 1º, 00:00 UTC)
Codificações simultâneas4 em andamento503 + 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

401X-Api-Key ausente ou inválida
400imagem não suportada ou corrompida
413payload 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.
429cota de taxa, diária ou mensal excedida, veja Retry-After. Não conta na cota.
503codificadores momentaneamente cheios, tente novamente após Retry-After
501modo armazenado solicitado, mas o armazenamento não está configurado

Corpos de erro são JSON: {"error": "..."}.