Visão geral
A cdnprogem é a API interna de processamento de arquivos (imagens e PDFs) com multitenancy. Todas as rotas de produto usam versionamento /v1/....
Convenções
- JSON em snake_case; datas em RFC 3339 UTC.
- Erros no formato RFC 9457 Problem Details (
type,title,status,detail,request_id). - Paginação cursor-based (nunca offset):
?cursor=...&limit=50→next_cursor. - Campo
projectnos uploads (padrão:default).
Fluxo assíncrono (jobs)
POST /v1/img/compress → 202 { job_id, status: "queued" }
GET /v1/jobs/{id} → 200 { status: "done", result: { output_url, ... } }
Operações pesadas (compress, resize, merge PDF, etc.) retornam 202 Accepted. Faça polling em GET /v1/jobs/{id} até status=done. O worker (make run-worker) deve estar rodando.
Placeholders
| Placeholder | Descrição |
|---|---|
BASE_URL | https://cdn.progem.com.br |
SUA_API_KEY | Token completo sk_test_... ou sk_live_... |
FILE_ID | UUID retornado no upload |
JOB_ID | UUID retornado ao enfileirar job |
PROJECT | Slug do projeto (padrão default) |
Autenticação — API Key
Rotas de produto exigem header Authorization: Bearer SUA_API_KEY. O tenant é resolvido automaticamente pela key — não envie header de tenant separado.
Formato da API key
sk_{env}_{prefix}_{secret}
Exemplo: sk_test_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
O segredo é exibido uma única vez na criação. Guarde-o com segurança.
Escopos
| Escopo | Permite |
|---|---|
files:read | GET /v1/files/*, download, polling de jobs |
files:write | POST e DELETE /v1/files/* |
img:write | Todas as rotas /v1/img/* e polling de jobs img |
pdf:write | Todas as rotas /v1/pdf/* e polling de jobs pdf |
admin:write | Bypass de todos os escopos acima |
Rotas públicas (sem auth)
GET /healthz,GET /readyzGET /v1/openapi.json
Erros comuns
| HTTP | Causa |
|---|---|
| 401 | API key ausente, inválida ou revogada |
| 403 | Escopo insuficiente para a rota |
| 402 | Quota do plano excedida |
| 429 | Rate limit atingido |
Imutabilidade e Delete
O upload em POST /v1/files aceita o campo multipart immutable. Esse flag define se o arquivo pode ser excluído ou processado via API.
Duas camadas de proteção
| Camada | Quem | O que protege |
|---|---|---|
| 1 — Anti-sobrescrita | Todos os arquivos (immutable=false ou omitido) |
Impossível alterar bytes após upload; permite delete + novo upload |
| 2 — Imutabilidade jurídica | Contratos assinados (immutable=true) |
Impossível delete, processamento pdf/img e qualquer modificação via API |
immutable=true vs immutable=false
| Aspecto | immutable=false (padrão) | immutable=true (contratos) |
|---|---|---|
| Quando usar | PDFs operacionais, documentos processáveis | Contratos assinados, retenção legal |
| Como definir | Omitir o campo ou valor ≠ true/1/yes | immutable=true somente no upload (irreversível) |
| Valores aceitos | — | true, 1, yes (case-insensitive) |
| Storage | private/ (files) ou public/ (img upload) | Sempre private/ |
| DELETE | Permitido → 204 | Bloqueado → 409 immutable_file |
| Processamento pdf/img | Permitido (cria variantes novas) | Bloqueado → 409 immutable_file |
| Corrigir erro | DELETE → novo upload → novo file_id | Novo documento no provedor → novo upload com novo file_id |
| Listagem | ?immutable=false | ?immutable=true |
| Escopo recomendado | files:read + files:write (+ pdf:write se processar) | Apenas files:read + files:write |
Como funciona o DELETE
DELETE /v1/files/{id} exige escopo files:write. Não existe scope separado files:delete — quem pode fazer upload também pode apagar (exceto imutáveis).
Arquivo normal (immutable=false):
- API valida tenant e busca metadados
- Remove registro no Postgres
- Remove objeto no storage (S3/MinIO)
- Retorna
204 No Content - Novo upload gera novo
file_ide nova storage key (UUID) — nunca reutiliza a anterior
Arquivo imutável (immutable=true):
HTTP/1.1 409 Conflict
{
"type": "https://imgflow.dev/problems/immutable-file",
"title": "Immutable file",
"status": 409,
"code": "immutable_file",
"detail": "This file is marked immutable and cannot be deleted or modified"
}
Metadados e bytes permanecem intactos no storage.
Fluxos resumidos
Upload operacional (immutable=false):
POST /v1/files → 201 { id, checksum, immutable: false }
... uso ...
DELETE /v1/files/{id} → 204
POST /v1/files (novo) → 201 { novo id }
Contrato assinado (immutable=true):
POST /v1/files + immutable=true → 201 { id, checksum, immutable: true }
GET /v1/files/{id}/download → 302 URL assinada (~15 min)
DELETE /v1/files/{id} → 409 immutable_file
POST /v1/pdf/compress { file_id } → 409 immutable_file
- Nunca usar
DELETEem contratos assinados - Nunca omitir
immutable=trueem documentos jurídicos - Nunca persistir URL de download (gerar nova a cada acesso; TTL ~15 min)
- Nunca rodar
/v1/pdf/*ou/v1/img/*sobre contratos assinados
GET Liveness — /healthz
Verifica se o processo está vivo. Sem autenticação.
GET /healthz
HTTP/1.1 200 OK
{ "status": "ok" }
GET Readiness — /readyz
Verifica dependências (Postgres, Redis, storage). Sem autenticação.
GET /readyz
HTTP/1.1 200 OK (ou 503 se degradado)
{
"status": "ok",
"checks": {
"postgres": "ok",
"redis": "ok",
"storage": "ok"
}
}
POST Upload de arquivo — /v1/files
Upload multipart autenticado. Grava sempre em private/. Valida magic bytes, sha256 e allowlist de extensões.
Ver também: Imutabilidade e Delete
POST /v1/files
Authorization: Bearer SUA_API_KEY
Content-Type: multipart/form-data
Campos:
file (obrigatório) — bytes do arquivo
project (opcional) — slug do projeto (padrão: default)
immutable (opcional) — true/1/yes para contratos assinados
# Upload operacional:
curl -X POST https://cdn.progem.com.br/v1/files \
-H "Authorization: Bearer SUA_API_KEY" \
-F "project=PROJECT" \
-F "file=@documento.pdf;type=application/pdf"
# Contrato assinado (imutável):
curl -X POST https://cdn.progem.com.br/v1/files \
-H "Authorization: Bearer SUA_API_KEY" \
-F "project=PROJECT" \
-F "immutable=true" \
-F "file=@contrato-assinado.pdf;type=application/pdf"
HTTP/1.1 201 Created
{
"id": "01900000-0000-7000-8000-000000000020",
"name": "contrato-123.pdf",
"extension": "pdf",
"mime": "application/pdf",
"media_type": "document",
"size": 5242880,
"checksum": "9f2c3a1b...",
"kind": "original",
"project_id": "01900000-0000-7000-8000-000000000001",
"immutable": true
}
Persista id e checksum imediatamente. Nunca persista URL de download.
201 Created — Arquivo gravado
400 Bad Request — Multipart inválido ou arquivo ausente
401 Unauthorized — API key inválida
403 Forbidden — Escopo files:write ausente
413 Payload Too Large — Excede limite do plano
422 Unprocessable — Extensão não permitida ou magic bytes inválidos
GET Listar arquivos — /v1/files
Listagem paginada por cursor. Filtro opcional por imutabilidade.
GET /v1/files?project=PROJECT&cursor=&limit=50&immutable=
Authorization: Bearer SUA_API_KEY
Query params:
project — slug do projeto (padrão: default)
cursor — cursor da página anterior (next_cursor)
limit — máximo 100 (padrão: 50)
immutable — true = só contratos; false = só operacionais; omitido = todos
# Listar contratos imutáveis:
curl "https://cdn.progem.com.br/v1/files?project=PROJECT&immutable=true&limit=50" \
-H "Authorization: Bearer SUA_API_KEY"
HTTP/1.1 200 OK
{
"data": [
{
"id": "01900000-0000-7000-8000-000000000020",
"name": "contrato-123.pdf",
"extension": "pdf",
"mime": "application/pdf",
"media_type": "document",
"size": 5242880,
"checksum": "9f2c3a1b...",
"kind": "original",
"project_id": "...",
"immutable": true
}
],
"next_cursor": "01900000-0000-7000-8000-000000000030"
}
200 OK — Lista retornada
401 Unauthorized — API key inválida
403 Forbidden — Escopo files:read ausente
422 Unprocessable — Parâmetro immutable inválido
GET Consultar metadados — /v1/files/{id}
GET /v1/files/FILE_ID
Authorization: Bearer SUA_API_KEY
HTTP/1.1 200 OK
{
"id": "FILE_ID",
"name": "documento.pdf",
"extension": "pdf",
"mime": "application/pdf",
"media_type": "document",
"size": 1234567,
"checksum": "abc123...",
"kind": "original",
"project_id": "...",
"immutable": false,
"original_size": 5000000,
"compression_percent": 75.3,
"url": "https://cdn.exemplo.com/..."
}
200 OK — Metadados retornados
401 Unauthorized — API key inválida
403 Forbidden — Escopo files:read ausente
404 Not Found — Arquivo não encontrado
GET Download — /v1/files/{id}/download
Retorna redirect 302 para URL assinada (CloudFront ou presigned S3). TTL máximo: 15 minutos. Gere uma URL nova a cada acesso.
GET /v1/files/FILE_ID/download
Authorization: Bearer SUA_API_KEY
curl -I https://cdn.progem.com.br/v1/files/FILE_ID/download \
-H "Authorization: Bearer SUA_API_KEY"
HTTP/1.1 302 Found
Location: https://cdn.exemplo.com/tenant/private/2026/09/document/arquivo.pdf?Expires=...&Signature=...&Key-Pair-Id=...
302 Found — Redirect para URL assinada
401 Unauthorized — API key inválida
403 Forbidden — Escopo files:read ausente
404 Not Found — Arquivo não encontrado
DELETE Excluir arquivo — /v1/files/{id}
Remove metadados e objeto no storage. Bloqueado para arquivos com immutable=true.
Ver detalhes: Imutabilidade e Delete
DELETE /v1/files/FILE_ID
Authorization: Bearer SUA_API_KEY
curl -X DELETE https://cdn.progem.com.br/v1/files/FILE_ID \
-H "Authorization: Bearer SUA_API_KEY"
# Arquivo operacional (immutable=false):
HTTP/1.1 204 No Content
# Arquivo imutável (immutable=true):
HTTP/1.1 409 Conflict
{
"type": "https://imgflow.dev/problems/immutable-file",
"title": "Immutable file",
"status": 409,
"code": "immutable_file",
"detail": "This file is marked immutable and cannot be deleted or modified"
}
204 No Content — Arquivo removido (immutable=false)
401 Unauthorized — API key inválida
403 Forbidden — Escopo files:write ausente
404 Not Found — Arquivo não encontrado
409 Conflict — Arquivo imutável (immutable_file)
POST Upload de imagem — /v1/img/upload
Upload público (public/). Dispara derive automático (thumbnail, optimized). Escopo: img:write.
POST /v1/img/upload
Authorization: Bearer SUA_API_KEY
Content-Type: multipart/form-data
Campos:
file (obrigatório)
project (opcional, padrão: default)
curl -X POST https://cdn.progem.com.br/v1/img/upload \
-H "Authorization: Bearer SUA_API_KEY" \
-F "project=PROJECT" \
-F "file=@foto.jpg;type=image/jpeg"
HTTP/1.1 201 Created
{
"id": "01900000-0000-7000-8000-000000000040",
"name": "foto.jpg",
"extension": "jpg",
"mime": "image/jpeg",
"media_type": "image",
"size": 2048000,
"checksum": "abc...",
"kind": "original",
"project_id": "...",
"width": 1920,
"height": 1080,
"url": "https://cdn.exemplo.com/tenant/public/..."
}
201 Created — Imagem gravada; derive enfileirado
401 Unauthorized — API key inválida
403 Forbidden — Escopo img:write ausente
422 Unprocessable — Conteúdo não é imagem válida
POST Resize — /v1/img/resize
Enfileira job de redimensionamento. Retorna 202.
POST /v1/img/resize
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"width": 800,
"height": 600,
"mode": "fit",
"upscale": false
}
HTTP/1.1 202 Accepted
{
"job_id": "01a05f31-efdf-7ed8-8664-945b39e86f65",
"status": "queued"
}
202 Accepted — Job enfileirado
401 Unauthorized — API key inválida
403 Forbidden — Escopo img:write ausente
404 Not Found — file_id não encontrado
409 Conflict — Arquivo imutável
POST Compress — /v1/img/compress
Enfileira job de compressão de imagem.
POST /v1/img/compress
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"quality": 80,
"max_dimension": 1920,
"max_bytes": 500000,
"format": "jpeg",
"strip_exif": true
}
HTTP/1.1 202 Accepted
{
"job_id": "01a05f31-efdf-7ed8-8664-945b39e86f65",
"status": "queued"
}
202 Accepted — Job enfileirado
401/403/404/409 — Ver img-resize
POST Convert — /v1/img/convert
Enfileira conversão de formato (webp, avif, jpeg, png).
POST /v1/img/convert
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"format": "webp"
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
POST Watermark — /v1/img/watermark
Enfileira aplicação de marca d'água usando outra imagem.
POST /v1/img/watermark
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"watermark_file_id": "WATERMARK_FILE_ID",
"opacity": 0.5,
"position": "bottom-right"
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
400 Bad Request — file_id ou watermark_file_id ausente
POST Compress Upload (síncrono) — /v1/pdf/compress/upload
Upload + compressão síncrona em um passo. Retorna 201 com PDF já comprimido. Grava em private/.
immutable=true). Para contratos, use POST /v1/files.
POST /v1/pdf/compress/upload
Authorization: Bearer SUA_API_KEY
Content-Type: multipart/form-data
Campos:
file (obrigatório)
project (opcional)
profile (opcional: screen|ebook|print)
sanitize (opcional: true/false)
strip_js (opcional: true/false)
target_compression_percent (opcional: 1-95, default tenant: 80)
curl -X POST https://cdn.progem.com.br/v1/pdf/compress/upload \
-H "Authorization: Bearer SUA_API_KEY" \
-F "project=PROJECT" \
-F "profile=ebook" \
-F "file=@documento.pdf"
HTTP/1.1 201 Created
{
"id": "01900000-0000-7000-8000-000000000050",
"name": "documento.pdf",
"extension": "pdf",
"mime": "application/pdf",
"media_type": "document",
"size": 8191590,
"checksum": "abc...",
"kind": "original",
"project_id": "...",
"original_size": 72520204,
"compression_percent": 88.7,
"target_missed": false,
"original_id": "01900000-0000-7000-8000-000000000051"
}
original_id presente quando keep_original=true (default). target_missed=true se a meta de compressão não foi atingida.
201 Created — PDF comprimido gravado
401/403 — Auth/escopo
422 Unprocessable — PDF inválido ou extensão bloqueada
POST Compress (assíncrono) — /v1/pdf/compress
Enfileira compressão. Aceita JSON (arquivo já gravado) ou multipart (upload + job).
# JSON — arquivo já no storage:
POST /v1/pdf/compress
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"profile": "ebook",
"sanitize": true,
"strip_js": true,
"target_compression_percent": 80
}
# Multipart — upload + job em um passo:
POST /v1/pdf/compress
Authorization: Bearer SUA_API_KEY
Content-Type: multipart/form-data
project=PROJECT
profile=ebook
file=@documento.pdf
HTTP/1.1 202 Accepted
{
"job_id": "01a05f31-efdf-7ed8-8664-945b39e86f65",
"status": "queued"
}
# Polling em GET /v1/jobs/{job_id} até status=done:
{
"id": "...",
"kind": "pdf.compress",
"status": "done",
"result": {
"output_file_id": "...",
"output_url": "https://...",
"size": 8191590,
"original_size": 72520204,
"compression_percent": 88.7,
"pages": 30,
"target_missed": false
}
}
202 Accepted — Job enfileirado
401 Unauthorized — API key inválida
403 Forbidden — Escopo pdf:write ausente
404 Not Found — file_id não encontrado
409 Conflict — Arquivo imutável (immutable_file)
POST Merge — /v1/pdf/merge
Enfileira merge de múltiplos PDFs em um.
POST /v1/pdf/merge
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_ids": [
"01900000-0000-7000-8000-000000000001",
"01900000-0000-7000-8000-000000000002"
]
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
409 Conflict — Algum file_id é imutável
POST Split — /v1/pdf/split
Enfileira divisão de PDF por intervalos de páginas (1-based).
POST /v1/pdf/split
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"ranges": [
{ "from": 1, "to": 5 },
{ "from": 6, "to": 10 }
]
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
POST Thumbnail — /v1/pdf/thumbnail
Enfileira geração de thumbnail de página PDF.
POST /v1/pdf/thumbnail
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"page": 1,
"width": 300,
"height": 400,
"dpi": 150
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
POST Rotate — /v1/pdf/rotate
Enfileira rotação de PDF.
POST /v1/pdf/rotate
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"degrees": 90
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
POST Protect — /v1/pdf/protect
Enfileira proteção por senha.
POST /v1/pdf/protect
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
{
"file_id": "FILE_ID",
"user_password": "senha123",
"owner_password": "admin456"
}
HTTP/1.1 202 Accepted
{ "job_id": "...", "status": "queued" }
202 Accepted — Job enfileirado
GET Consultar job — /v1/jobs/{id}
Polling de status de job assíncrono. Escopos: files:read, img:write, pdf:write ou admin:write.
GET /v1/jobs/JOB_ID
Authorization: Bearer SUA_API_KEY
# Consulta única:
curl -sS "https://cdn.progem.com.br/v1/jobs/JOB_ID" \
-H "Authorization: Bearer SUA_API_KEY"
# Polling — repetir até status=done ou failed:
while true; do
STATUS=$(curl -sS "https://cdn.progem.com.br/v1/jobs/JOB_ID" \
-H "Authorization: Bearer SUA_API_KEY" | jq -r '.status')
echo "status=$STATUS"
case "$STATUS" in done|failed) break ;; esac
sleep 2
done
# Em processamento:
HTTP/1.1 200 OK
{
"id": "01a05f31-efdf-7ed8-8664-945b39e86f65",
"kind": "pdf.compress",
"status": "queued",
"created_at": "2026-09-03T22:00:00Z"
}
# Concluído:
HTTP/1.1 200 OK
{
"id": "01a05f31-efdf-7ed8-8664-945b39e86f65",
"kind": "pdf.compress",
"status": "done",
"created_at": "2026-09-03T22:00:00Z",
"finished_at": "2026-09-03T22:00:15Z",
"result": {
"output_file_id": "01900000-0000-7000-8000-000000000060",
"output_url": "https://cdn.exemplo.com/...",
"size": 8191590,
"original_size": 72520204,
"compression_percent": 88.7,
"pages": 30,
"target_missed": false,
"variants": { "optimized": "...", "thumbnail": "..." },
"variants_urls": {
"optimized": "https://cdn.exemplo.com/...",
"thumbnail": "https://cdn.exemplo.com/..."
}
}
}
# Falhou:
{
"id": "...",
"kind": "img.compress",
"status": "failed",
"error": "processing failed: ..."
}
Status possíveis: queued, running, done, failed.
200 OK — Job retornado
401 Unauthorized — API key inválida
403 Forbidden — Escopo insuficiente
404 Not Found — Job não encontrado