cdnprogem

Documentação da API — processamento de arquivos, imagens e PDFs

Abrir OpenAPI (JSON) Baixar coleção Postman

Base URL: https://cdn.progem.com.br

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/....

Setup administrativo: criação de tenant e API key é feita pelo time interno (rotas administrativas). Solicite sua API key ao admin antes de integrar.

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=50next_cursor.
  • Campo project nos 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

PlaceholderDescrição
BASE_URLhttps://cdn.progem.com.br
SUA_API_KEYToken completo sk_test_... ou sk_live_...
FILE_IDUUID retornado no upload
JOB_IDUUID retornado ao enfileirar job
PROJECTSlug 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

EscopoPermite
files:readGET /v1/files/*, download, polling de jobs
files:writePOST e DELETE /v1/files/*
img:writeTodas as rotas /v1/img/* e polling de jobs img
pdf:writeTodas as rotas /v1/pdf/* e polling de jobs pdf
admin:writeBypass de todos os escopos acima

Rotas públicas (sem auth)

  • GET /healthz, GET /readyz
  • GET /v1/openapi.json

Erros comuns

HTTPCausa
401API key ausente, inválida ou revogada
403Escopo insuficiente para a rota
402Quota do plano excedida
429Rate 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

CamadaQuemO 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

Aspectoimmutable=false (padrão)immutable=true (contratos)
Quando usarPDFs operacionais, documentos processáveisContratos assinados, retenção legal
Como definirOmitir o campo ou valor ≠ true/1/yesimmutable=true somente no upload (irreversível)
Valores aceitostrue, 1, yes (case-insensitive)
Storageprivate/ (files) ou public/ (img upload)Sempre private/
DELETEPermitido204Bloqueado409 immutable_file
Processamento pdf/imgPermitido (cria variantes novas)Bloqueado409 immutable_file
Corrigir erroDELETE → novo upload → novo file_idNovo documento no provedor → novo upload com novo file_id
Listagem?immutable=false?immutable=true
Escopo recomendadofiles: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):

  1. API valida tenant e busca metadados
  2. Remove registro no Postgres
  3. Remove objeto no storage (S3/MinIO)
  4. Retorna 204 No Content
  5. Novo upload gera novo file_id e 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
Anti-padrões:
  • Nunca usar DELETE em contratos assinados
  • Nunca omitir immutable=true em 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/.

Não use em contratos assinados (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