Catalisa.Building Blocks
Catálogo/Documentos/Document Template

Document Template

Beta

Contratos e documentos gerados a partir de template versionado, com variáveis tipadas

11
Endpoints
3
Entidades
1
Provedores
Tenant
Escopo
3019
Porta
2026-01
Desde

O contrato que o seu cliente assina para de morar dentro do código do seu sistema. Vira um template versionado, que o jurídico edita, publica e consegue provar, dois anos depois, exatamente qual redação foi usada naquela operação.

Para quem é
  • Financeiras e fintechs de crédito que emitem CCB, contrato e aditivo em cada operação
  • Operações reguladas que precisam provar qual versão do contrato o cliente aceitou
  • Times de produto que hoje montam HTML de contrato dentro do código da aplicação
Substitui
  • Serviço externo de geração de PDF por API cobrado por documento
  • Template de contrato colado dentro do código-fonte da aplicação
  • Planilha ou pasta compartilhada com as versões do contrato em Word
O que não é
  • Um editor visual de documento para o usuário final (não há interface, é API)
  • Uma ferramenta de assinatura eletrônica (isso é o e-signature)
  • Um gerador de relatório analítico ou de dashboard
O que dá para fazer

12 endpoints em 3 recursos.

Explorar a API →
01

Resumo executivo

O Document Template guarda o texto dos seus documentos — contrato, CCB, proposta, recibo — fora do código, com marcadores no lugar dos dados que mudam a cada operação. Você envia os valores, ele devolve o documento pronto.

Na prática, resolve o dia em que o jurídico manda trocar uma cláusula do contrato de crédito. Sem ele, isso vira ticket de engenharia, deploy e a dúvida de quais contratos já emitidos usaram a redação antiga. Com ele, o texto é editado, publicado como uma nova versão, e todo documento emitido guarda o número da versão que o gerou — a pergunta "qual redação esse cliente assinou" vira uma consulta.

Está em beta desde janeiro de 2026. Roda no monolito e em standalone na porta 3019, e está declarado nas pilhas de staging e de produção. A saída em HTML funciona; a saída em PDF depende de um serviço acompanhante que não está publicado em nenhum ambiente — leia a §15 antes de prometer PDF a cliente.

AtributoValor
Identificadordocument-template
CategoriaDocumentos
EscopoTenant (exige organizationId no token, em todas as rotas)
Porta (standalone)3019
Path alias@document-template
Prefixo HTTP/document-templates
StatusBeta desde 2026-01
Depende dePostgreSQL (schema doctemplates), Redis, serviço pdf-renderer (opcional)
PermissõesDOCUMENT_TEMPLATES_CREATE, _READ, _UPDATE, _DELETE, _PUBLISH, _RENDER

02

O problema

negócio

O cenário. Uma financeira fecha uma operação de crédito. No fim da esteira, alguém precisa emitir a Cédula de Crédito Bancário com o nome do devedor, o CPF, o valor principal, a taxa, o CET, o número de parcelas, o IOF e a data do primeiro vencimento. O documento tem que sair correto, com a redação vigente, e tem que continuar comprovável depois.

O que trava hoje.

  • O contrato mora no código. O HTML do documento fica dentro do repositório da aplicação. Trocar uma vírgula de cláusula exige pull request, revisão, build e deploy — para uma mudança que o jurídico deveria conseguir fazer sozinho.
  • Ninguém sabe qual versão foi usada. O código só tem a versão de hoje. Se o contrato mudou três vezes no ano, descobrir qual texto o cliente de março recebeu significa procurar no histórico do Git e torcer para a data do deploy bater com a data da operação.
  • Campo em branco vira contrato inválido. Sem validação do conjunto de variáveis, um dado ausente na esteira imprime undefined, ou pior, imprime um espaço vazio no meio de uma cláusula financeira e ninguém percebe.
  • Cada time reinventa a mesma coisa. Um serviço monta HTML por concatenação, outro usa uma biblioteca de template, um terceiro chama um serviço externo. Três formas de errar e nenhuma trilha comum.
  • O serviço externo leva o dado do contrato para fora. Enviar nome, CPF, valor e condições de crédito a um conversor de PDF de terceiro é uma transferência de dado pessoal que precisa de base legal e de contrato de operador. Costuma ser feito sem nenhum dos dois.

O custo de não resolver. O custo visível é a fila de engenharia para mudar texto jurídico. O custo caro aparece depois: numa disputa ou numa fiscalização, "não conseguimos reproduzir exatamente o documento que foi emitido naquela data" é uma resposta que custa mais do que qualquer sprint. A CCB é título executivo extrajudicial por força do art. 28 da Lei nº 10.931/2004, e o art. 29 lista seis requisitos essenciais de forma. Um documento gerado sem controle de versão é um documento que você não consegue defender — e um requisito essencial que saiu em branco é um título cuja executividade alguém vai questionar.


03

Proposta de valor

negócio
AntesDepois
O texto do contrato está dentro do código da aplicaçãoO texto é um recurso versionado, editável sem deploy
"Qual redação o cliente de março aceitou?" é investigação no GitCada documento emitido carrega o número da versão que o gerou
Dado faltando imprime campo vazio no contratoVariável obrigatória ausente devolve 400 antes de gerar qualquer coisa
Cada serviço monta documento do seu jeitoUm catálogo de templates, uma engine, um formato de saída
O contrato trafega para um conversor de PDF de terceiroO conteúdo não sai da sua infraestrutura

Publicar congela a versão. POST /publish copia o conteúdo e a declaração de variáveis para uma linha imutável em document_template_versions. Editar o template depois muda o rascunho de trabalho, não a versão publicada. Renderização sempre usa a última versão publicada — nunca o rascunho.

A variável tem tipo e é conferida. Cada template declara suas variáveis com nome, tipo (STRING, NUMBER, BOOLEAN, DATE, ARRAY, OBJECT), obrigatoriedade e valor padrão. Antes de renderizar, o serviço confere presença e tipo. Falta de dado é erro de validação, não é contrato com buraco.

Toda emissão vira registro. Cada renderização grava uma linha em rendered_documents com os valores de entrada, o HTML produzido, a versão usada e o identificador do negócio. É a diferença entre saber que um contrato foi gerado e conseguir reproduzi-lo.

O documento nasce dentro da operação. O campo businessId amarra o documento à proposta, ao pedido ou ao contrato que o originou, e o organizationId vem do token. O documento gerado já sabe de quem é e a que operação pertence.


04

Casos de uso reais

negócio

Os quatro casos abaixo cobrem os três motivos pelos quais uma operação adota este building block: tirar o texto jurídico do código, impedir que dado faltando vire contrato com buraco, e reunir documentos diferentes num catálogo só. O quarto é uma leitura do mercado brasileiro, não um cliente.

flowchart LR
  C1["Caso 1 · redação muda sem deploy"] --> D["Document Template"]
  C2["Caso 2 · contrato não sai com campo vazio"] --> D
  C3["Caso 3 · proposta, CCB e recibo no mesmo catálogo"] --> D
  D --> R["Documento emitido, com versão registrada"]

Caso 1 — Uma financeira muda cláusula de CCB sem pedir deploy Cenário ilustrativo

Contexto

Financeira de crédito pessoal que emite entre 800 e 1.200 CCBs por mês, cada uma com valor, taxa, CET, IOF e cronograma calculados na esteira.

A dor

O texto da cédula estava dentro do código do serviço de originação. Toda alteração pedida pelo jurídico — inclusão da cláusula de portabilidade, ajuste na redação da mora — virava um ciclo de duas semanas entre chamado, desenvolvimento, revisão e janela de deploy. E como o código só guarda a versão atual, ninguém conseguia responder com segurança qual texto tinha sido emitido em determinada data.

A solução com o BB

A cédula vira um template com quinze variáveis declaradas (valor_principal, taxa_mensal, cet_anual, num_parcelas, iof_total, sistema_amortizacao, garantias, entre outras). O jurídico revisa o conteúdo, o time chama PATCH no template e depois POST .../publish com o changelog da mudança. A esteira continua chamando POST .../render com os valores calculados; ela nem sabe que a redação mudou.

flowchart LR
  J["Jurídico revisa a redação"] --> P["PATCH no template (rascunho)"]
  P --> PV["POST de publish com changelog"]
  PV --> V["Versão imutável nova"]
  E["Esteira de originação"] --> RD["POST de render, sempre a última versão"]
  V --> RD
  RD --> DOC["CCB emitida, com o número da versão gravado"]
O resultado

Mudança de cláusula deixa de exigir deploy. E GET .../versions responde, em uma chamada, quais versões existiram, quando cada uma foi publicada e o que mudou em cada uma.

sequenceDiagram
  autonumber
  participant App as Esteira de crédito
  participant DT as Document Template
  participant FS as File Storage
  participant ES as E-Signature
  App->>DT: "POST de render/pdf com as variáveis da CCB e o businessId"
  DT-->>App: "PDF em base64 e o templateVersionNumber"
  App->>FS: "POST de arquivo com o PDF"
  FS-->>App: "fileId"
  App->>ES: "POST de solicitação de assinatura com fileId, signatários e businessId"
  ES-->>App: "id da solicitação e viewUrl para o signatário"

Caso 2 — A esteira para de emitir contrato com campo vazio Cenário ilustrativo

Contexto

Operação de crédito consignado em que o valor do IOF é calculado por um serviço separado e chega assíncrono à etapa de emissão do contrato.

A dor

Em uma fração das operações o cálculo de IOF atrasava e o contrato saía com o campo em branco. O problema só era detectado quando o cliente reclamava — ou, pior, quando não reclamava. Cada ocorrência exigia cancelar o contrato, reemitir e refazer a coleta de assinatura.

A solução com o BB

O template declara iof_total como NUMBER obrigatório. Se o valor não chega, POST .../render devolve 400 com Required variable 'iof_total' is missing e nenhum documento é gerado, nenhuma linha é gravada e nenhuma assinatura é solicitada. A esteira trata o 400 como "ainda não pronto" e reprocessa.

flowchart LR
  A["Esteira chama o render com as variáveis"] --> B{"iof_total presente e numérico?"}
  B -->|Não| C["400 · Required variable iof_total is missing"]
  C --> D["Nada é gerado, nada é gravado, nada é assinado"]
  D --> E["Esteira aguarda o cálculo e tenta de novo"]
  E --> A
  B -->|Sim| F["Contrato completo, emissão registrada"]
O resultado

A classe inteira de erro deixa de existir na saída. O custo se move de "reemitir contrato assinado" para "esperar mais trinta segundos".

Caso 3 — Documentos diferentes, uma cadeia só Cenário ilustrativo

Contexto

Fintech que emite, no mesmo fluxo, a proposta comercial, a CCB e o recibo de liberação.

A dor

Cada documento tinha sido resolvido de um jeito em um momento diferente: a proposta era montada no front, a CCB no backend, e o recibo vinha de uma planilha exportada. Três formatos, três lugares para mudar layout, e nenhuma visão de quantos documentos a operação emitia.

A solução com o BB

Os três viram templates com categorias distintas (PROPOSAL, CONTRACT, RECEIPT) dentro do mesmo catálogo. A emissão é a mesma chamada em todos os casos, mudando só o templateId e as variáveis. O businessId é o mesmo nos três, então listar tudo que foi emitido para uma operação é filtrar por um campo.

flowchart LR
  OP["Operação · businessId proposta-2026-000481"] --> T1["Template PROPOSAL"]
  OP --> T2["Template CONTRACT · a CCB"]
  OP --> T3["Template RECEIPT"]
  T1 --> R["Mesma chamada de render, só muda o templateId"]
  T2 --> R
  T3 --> R
  R --> L["Três emissões com o mesmo businessId"]
O resultado

Um catálogo, uma forma de emitir, uma consulta que responde "o que já foi gerado para esta proposta".

Caso 4 — Quem emite CCB no Brasil resolve o template com gente, não com produto Referência de mercado

Contexto

A Qi Tech é uma das plataformas brasileiras mais transparentes sobre emissão de dívida, e publica documentação aberta do fluxo de emissão de CCB, CCI e CCE (Qi Tech — emissão de dívida, consulta em 2026-08-16).

A dor

A dor aqui é do mercado inteiro, não de um cliente. A própria documentação descreve o caminho: "com a minuta da operação de crédito definida, o time de suporte QI Tech realizará a parametrização do contrato na plataforma e seu PDF poderá ser emitido". Ou seja, o cliente envia a minuta e um time humano parametriza o contrato dentro da plataforma — não há upload de template pelo cliente, nem linguagem de template exposta, nem autonomia para mudar uma cláusula. Cada revisão jurídica vira um pedido ao fornecedor. Na Celcoin, a CCB aparece nomeada como produto da linha de crédito (celcoin.com.br, consulta em 2026-08-16), sem detalhamento público do mecanismo de geração do documento.

flowchart LR
  subgraph MODELO_FORNECEDOR["Modelo do fornecedor"]
    A1["Você envia a minuta"] --> A2["Time humano parametriza o contrato"]
    A2 --> A3["PDF emitido pela plataforma"]
    A4["Nova revisão jurídica"] --> A1
  end
  subgraph MODELO_CATALISA["Modelo Catalisa"]
    B1["Você cria e edita o template por API"] --> B2["POST de publish com changelog"]
    B2 --> B3["Documento emitido, versão registrada"]
    B4["Nova revisão jurídica"] --> B1
  end
A solução com o BB

Como a Catalisa endereça: o template é um recurso da sua organização, que você cria, edita, publica e versiona por API, sem intermediário. A revisão do jurídico vira PATCH mais POST /publish, com changelog registrado, e não um chamado com prazo de fornecedor.

O resultado

A autonomia sobre a redação do contrato fica com quem responde juridicamente por ela. Vale registrar a leitura honesta do mercado: nenhum dos fornecedores brasileiros que conseguimos verificar publica motor de template self-service para contrato de crédito — o que pode significar espaço em aberto ou risco que o mercado deliberadamente não quis assumir. Não temos evidência pública para decidir qual das duas.


05

Mercado e diferenciais

negócio

Panorama — os três caminhos de hoje

Panorama. Quem precisa gerar documento hoje escolhe entre três caminhos. O primeiro é um serviço por API — DocRaptor, PDFMonkey, Docmosis, Carbone —, que resolve a conversão muito bem e cobra por documento, por página ou por faixa de volume. O segundo é fazer em casa com WeasyPrint ou Puppeteer, o que elimina a licença e cria uma operação. O terceiro, mais comum do que se admite, é montar HTML dentro do código da aplicação e imprimir.

flowchart TD
  N["Preciso gerar contrato a partir de dados"] --> C1["Serviço por API"]
  N --> C2["Renderizador em casa"]
  N --> C3["HTML montado dentro do código"]
  C1 --> P1["Cobra por documento, página ou faixa"]
  C2 --> P2["Sem licença, com operação a manter"]
  C3 --> P3["Sem custo aparente, e sem trilha nenhuma"]
  P1 --> L["Nenhum dos três prova qual redação foi usada"]
  P2 --> L
  P3 --> L

O wkhtmltopdf saiu da mesa

Um aviso útil para quem está escolhendo: o wkhtmltopdf saiu da mesa. O repositório foi arquivado em janeiro de 2023 e está em modo somente leitura (wkhtmltopdf no GitHub, consulta em 2026-08-16). Ele roda sobre um fork do QtWebKit baseado em Qt 4.8 — um motor de navegador descontinuado há mais de uma década, sem CSS moderno e sem correção de segurança. Se a sua avaliação ainda o inclui, exclua.

A lacuna que o Document Template ocupa

Nenhum dos três caminhos resolve o que dói de verdade em operação regulada: provar qual redação foi usada em qual documento. Essa é a lacuna que o Document Template ocupa. Ele não compete em fidelidade tipográfica com o PrinceXML do DocRaptor; compete em ciclo de vida do documento dentro de uma plataforma multi-tenant.

Comparativo lado a lado

CritérioCatalisa Document TemplateDocRaptorCarbone.ioDocmosisPDFMonkeyWeasyPrint em casa
Preço público (2026-08-16)Em definiçãoUS$ 15 a US$ 1.000/mês por faixaUS$ 29 a US$ 595/mês; US$ 1.500 a US$ 2.940/ano auto-hospedadoUS$ 49 a US$ 78/mês; US$ 3.795 a US$ 17.145 perpétua€ 5 a € 300/mêsSem licença
Formato do templateHTML com HandlebarsHTML/CSSDOCX, ODT, XLSXDOCX, ODTHTML com LiquidO que você escrever
Editado por não-engenheiroNão (é API; o conteúdo é HTML)NãoSim, no WordSim, no WordParcial, no painelNão
Versionamento imutável com changelogSim, nativoNãoNãoNãoNãoVocê implementa
Variáveis tipadas e validadasSim, recusa dado faltandoNãoNãoNãoNãoVocê implementa
Histórico de emissão consultávelSim, com valores de entradaNãoNãoNãoParcialVocê implementa
Isolamento multi-tenantSim, pelo token do IAMVocê implementaVocê implementaVocê implementaVocê implementaVocê implementa
Saída em PDFSim, via serviço acompanhante (ver §15)SimSimSimSimSim
Motor de renderizaçãoO que você acoplarPrinceXMLLibreOffice headlessLibreOfficeChromeWeasyPrint
CSS Paged Media completoDepende do acopladoSimParcialParcialNãoSim
Saída em DOCXNãoNãoSimSimNãoNão
O dado sai da sua infraestruturaNãoSimSim na nuvem, não no auto-hospedadoSim na nuvem, não na perpétuaSimNão

Preços consultados nas páginas oficiais em 2026-08-16: DocRaptor (a URL /pricing retorna 404; a tabela está no cadastro), Carbone, Docmosis e PDFMonkey. Todos variam por faixa, região e negociação — confira na data da sua análise. Os valores são dos fornecedores, não estimativas nossas.

Nossos diferenciais

Nossos diferenciais

  1. Publicação congela conteúdo e contrato de dados juntos. A versão publicada guarda o HTML e a declaração de variáveis daquele momento. Reproduzir um documento antigo não depende de lembrar quais campos existiam na época — a versão sabe. É difícil de copiar não pela mecânica, mas porque exige tratar o documento como recurso versionado desde o primeiro dia, e não como string.
  2. A validação acontece antes da geração. Variável obrigatória ausente, tipo errado ou data inválida param a requisição com 400. Um conversor de HTML em PDF não tem como fazer isso: para ele, o HTML já chega pronto, com o buraco dentro.
  3. A emissão é prova, não log. rendered_documents guarda os valores de entrada, a versão usada, o HTML gerado e o vínculo com a operação. É o que permite responder a um questionamento sem depender de backup.
  4. O tenant vem do token e nunca do corpo. Templates e documentos são isolados por organização no banco. Em serviço externo, isolamento entre clientes é problema seu — e problema que só aparece no dia do incidente.

Quando escolher o concorrente

Quando escolher o concorrente. Cinco situações em que a resposta honesta não é o Document Template. Cada linha traz o requisito, para quem ele aponta e o motivo.

Se o seu requisito éEscolhaPor quê
Tipografia de impressão de alta exigência — cabeçalho e rodapé correntes de verdade, sumário com número de página, controle de viúvas e órfãsDocRaptor com PrinceXMLEntrega hoje uma qualidade que um Chrome headless não alcança, e o Document Template depende do renderizador que você acoplar
Que o jurídico edite o template sozinho no WordCarbone e DocmosisResolvem isso e nós não: aqui o conteúdo é HTML, e HTML não é ferramenta de advogado
Saída em DOCX ou ODTCarbone e DocmosisEsses dois atendem e nós não geramos nenhum dos dois
Volume alto, previsível e com custo fixoLicença perpétua do DocmosisSai na frente de qualquer assinatura
Algumas centenas de PDFs simples por mês, sem exigência de rastreabilidadePDFMonkey a € 15 por 3 mil documentosCusta menos esforço do que qualquer coisa que você construa

O Document Template ganha quando o documento é jurídico, recorrente, multi-cliente e precisa ser reproduzível anos depois — não quando o problema é converter HTML em PDF bonito.

Uma nota para quem for acoplar o renderizador de PDF

Uma nota para quem for acoplar o renderizador de PDF: se o documento é gerado no servidor a partir de dados, como é o caso de contrato e CCB, o WeasyPrint é a escolha mais defensável entre as opções livres. Ele implementa CSS Paged Media a sério — @page, margin boxes, elementos correntes de cabeçalho e rodapé, referências cruzadas — enquanto o Chrome headless só oferece headerTemplate e footerTemplate em contexto isolado.

O WeasyPrint não executa JavaScript, o que é irrelevante para contrato e decisivo para relatório com gráfico interativo. Em contrapartida, o Chrome consome de 300 a 500 MB por instância, vaza memória e exige pool com reciclagem; o WeasyPrint é um processo Python que sobe em milissegundos (documentação do WeasyPrint e opções do Puppeteer, consulta em 2026-08-16).

Critério do acoplamentoWeasyPrintChrome headless via Puppeteer
CSS Paged MediaCompleto — @page, margin boxes, elementos correntes, referências cruzadasheaderTemplate e footerTemplate, em contexto isolado
Executa JavaScriptNãoSim
Consumo de memóriaProcesso Python que sobe em milissegundosDe 300 a 500 MB por instância, com vazamento e pool a reciclar

06

Modelo de cobrança e ROI

negócio

Unidade de cobrança

Precificação em definição. O Document Template não tem preço fechado. Ele é uma peça da esteira de documentos e a intenção é que acompanhe a contratação do conjunto — geração, assinatura e leitura — em vez de ser vendido isolado. Não há valor a divulgar e este documento não estima nenhum.

O que dispara custo

O que dispara custo.

DriverPor quê
Documentos renderizados por mêsCada renderização compila o template, valida as variáveis e grava uma linha com o HTML produzido
Proporção de saída em PDFPDF envolve uma chamada de rede a um renderizador com até 60 segundos de espera, e é a operação mais cara do fluxo
Número de templates ativos e versõesCada publicação guarda uma cópia integral do conteúdo; templates grandes com muitas versões crescem o armazenamento
Tamanho do documento geradoO HTML de saída é gravado integralmente em rendered_documents

Comparação de custo num cenário nomeado

Comparação de custo — cenário nomeado: financeira que emite 3.000 documentos por mês (CCB, proposta e recibo somados), sendo 2.000 deles em PDF.

Catalisa Document TemplateDocRaptorCarbone (nuvem)Docmosis (perpétua)PDFMonkey
Base de cálculoPrecificação em definiçãoFaixa de 5.000 docs: US$ 149/mêsPlano com cota mensal: a partir de US$ 29/mêsLicença única T-400: US$ 6.695Plano Pro: € 15/mês por 3.000 docs
Versionamento e históricoInclusoVocê implementaVocê implementaVocê implementaVocê implementa
Isolamento por clienteInclusoVocê implementaVocê implementaVocê implementaVocê implementa
Dado do contrato sai da sua redeNãoSimSimNãoSim

Preços dos fornecedores consultados em 2026-08-16 nas páginas citadas na §5. As faixas mudam por região e negociação; a linha "você implementa" é o que separa preço de custo total, e é onde a comparação de licença engana.

ROI — a conta de guardanapo

ROI. A conta de guardanapo tem duas linhas, e nenhuma delas é licença de conversor.

flowchart LR
  R["ROI do Document Template"] --> L1["Linha 1 · ciclo de mudança de texto jurídico"]
  R --> L2["Linha 2 · custo de não conseguir reproduzir"]
  L1 --> A1["Antes · análise, desenvolvimento, revisão e janela de deploy"]
  L1 --> D1["Depois · uma chamada de API e uma publicação"]
  L2 --> A2["Zero até o dia em que não é"]
  L2 --> D2["Nesse dia, apresentar o documento exato daquela data"]

Linha 1 — o ciclo de mudança de texto jurídico. A primeira é o ciclo de mudança de texto jurídico. Com o template dentro do código, cada alteração pedida pelo jurídico consome análise, desenvolvimento, revisão e janela de deploy. Com o template como recurso, consome uma chamada de API e uma publicação. Numa operação de crédito, que revisa contrato várias vezes por ano por mudança regulatória, a diferença é medida em semanas de engenharia por ano.

Linha 2 — o custo de não conseguir reproduzir. A segunda é o custo de não conseguir reproduzir. Ele é zero até o dia em que não é — e nesse dia é uma disputa judicial ou uma resposta a fiscalização em que a sua defesa depende de conseguir apresentar exatamente o documento emitido, com a redação vigente naquela data. Esse item não entra em planilha de economia, mas é o que sustenta a decisão de compra em operação regulada.


07

Arquitetura

As camadas e o caminho da requisição

flowchart TD
  HTTP["HTTP · Bearer JWT emitido pelo IAM"]
  subgraph APP["Hono app · basePath /document-templates · applyCommonMiddleware"]
    TR["templateRouter em /api/v1/document-templates · 8 rotas"]
    RR["renderRouter em /api/v1/document-templates · 3 rotas"]
    HL["/health · sonda pública"]
  end
  HTTP --> APP
  APP --> PIPE["Zod parse → ResultAsync de T e AppError → handleResult"]
  subgraph SVC["services/ e utils/"]
    TS["TemplateService · criar, atualizar, publicar, excluir"]
    RS["RenderService · renderizar em HTML ou PDF, pré-visualizar"]
    PC["PDFClient · cliente HTTP do renderizador, opcional"]
    VP["utils/variable-parser · validação de tipo e obrigatoriedade"]
  end
  PIPE --> SVC
  subgraph REPO["repositories/ · 3, Prisma"]
    DB[("PostgreSQL · schema doctemplates")]
    T1["document_templates"]
    T2["document_template_versions"]
    T3["rendered_documents"]
    DB --- T1
    DB --- T2
    DB --- T3
  end
  SVC --> REPO
  SVC --> HB["Handlebars compila o conteúdo · escapa HTML por padrão"]
  HB -->|"só quando PDF"| PDFR["pdf-renderer acompanhante · POST /api/v1/render-pdf · timeout de 60s → base64"]

O elo entre as duas primeiras camadas, em notação de código, é Zod parse → ResultAsync<T, AppError> → handleResult. A compilação do conteúdo é Handlebars.compile(conteúdo), que escapa HTML por padrão; na renderização, Handlebars.compile(versao.conteudo)(dados).

Caminho de uma renderização em HTML

Caminho de uma renderização em HTML

flowchart TD
  A["POST /:templateId/render"] --> B{"Template existe e pertence à organização do token?"}
  B -->|Não| B404["404"]
  B -->|Sim| C{"status igual a PUBLISHED?"}
  C -->|Não| C400["400 · Template must be published"]
  C -->|Sim| D["Busca a ÚLTIMA versão publicada, nunca o rascunho de trabalho"]
  D --> E["Aplica os valores padrão declarados nas variáveis da VERSÃO"]
  E --> F{"Obrigatoriedade e tipo conferem?"}
  F -->|Falha| F400["400 · VALIDATION"]
  F -->|Ok| G["Handlebars aplica os dados ao conteúdo da versão"]
  G --> H["Grava rendered_documents · entrada, HTML, versão e businessId"]
  H --> I["Publica document-templates.document.rendered → 200"]

O fluxo completo: template, dados e documento

O caminho real de ponta a ponta soma três atores que a lista de camadas não mostra junta: quem escreve o template, quem manda os dados e quem guarda o resultado. O diagrama abaixo mostra a persistência do RenderedDocument e o ponto em que o fileStorageId deixa de ser preenchido — a integração com o file-storage não existe hoje (§15), e é a aplicação chamadora que guarda o binário.

sequenceDiagram
  autonumber
  participant Aut as Autor do template
  participant DT as Document Template
  participant PG as PostgreSQL doctemplates
  participant App as Aplicação chamadora
  participant PR as pdf-renderer acompanhante
  participant FS as File Storage
  Aut->>DT: "POST de template e PATCH do rascunho"
  DT->>PG: "Grava document_templates em DRAFT"
  Aut->>DT: "POST de publish com changelog"
  DT->>PG: "Grava document_template_versions · versão imutável"
  App->>DT: "POST de render com variáveis e businessId"
  DT->>PG: "Lê a última versão publicada"
  DT->>DT: "Valida variáveis e compila o Handlebars"
  alt Saída em HTML
    DT->>PG: "Grava rendered_documents com inputData e outputHtml"
    DT-->>App: "200 com o HTML e o templateVersionNumber"
  else Saída em PDF
    DT->>PR: "POST /api/v1/render-pdf com o HTML · até 60s"
    PR-->>DT: "Binário do PDF"
    DT->>PG: "Grava rendered_documents · fileStorageId fica nulo"
    DT-->>App: "200 com pdfBase64 e o templateVersionNumber"
    App->>FS: "Upload do PDF, hoje por conta da aplicação"
  end

Decisões não óbvias

Decisões não óbvias.

  • Renderizar usa a versão publicada; pré-visualizar usa o rascunho. POST /:id/renderdocument_template_versions; POST /:id/preview lê o conteúdo corrente do template. É proposital: a pré-visualização existe para quem está escrevendo o template e precisa ver o efeito antes de publicar. Consequência prática — a pré-visualização não valida variável obrigatória, justamente para permitir ver um documento pela metade durante a edição. Não use preview como ensaio de produção.
  • Editar um template publicado é permitido. A edição altera o rascunho de trabalho, não a versão publicada, e não afeta nenhuma renderização até que alguém chame POST /publish. O trade-off é que o GET do template mostra o rascunho, não o que está sendo emitido — para ver o que está no ar, consulte a versão.
  • Handlebars escapa HTML por padrão. {{variavel}} escapa o conteúdo. Um valor com < ou & sai como entidade, o que é o comportamento correto quando o dado vem do cliente. Para injetar HTML deliberadamente — uma tabela de parcelas montada fora, por exemplo — use {{{variavel}}}, sabendo que a responsabilidade pelo conteúdo passa a ser sua.
  • O PDF é delegado, não embutido. Não há Chrome dentro do serviço. O PDFClient chama um renderizador por HTTP, com 60 segundos de limite, e recebe o PDF em base64. A razão é operacional: um navegador headless no mesmo processo transforma o consumo de memória do serviço em algo imprevisível, e a renderização de documento é justamente o pico. O custo dessa escolha é que sem PDF_RENDERER_URL configurada, a rota de PDF devolve 400 — e hoje ela não está configurada em nenhum ambiente publicado (§15).
  • A versão guarda conteúdo e variáveis juntos. Poderia guardar só o conteúdo e apontar para as variáveis do template. Não guarda, de propósito: se as variáveis mudarem depois, reproduzir um documento antigo com a declaração nova produziria um resultado diferente do original. A versão precisa ser autossuficiente para ser prova.
  • O documento renderizado é gravado integralmente. rendered_documents guarda inputData e outputHtml. Isso é o que permite reproduzir e auditar — e é também o motivo de a §14 tratar essa tabela como repositório de dado pessoal.

Monolito vs. standalone

Monolito vs. standalone. O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/document-templates. O main.ts sobe o mesmo app com Bun.serve na porta 3019 quando DEPLOYMENT_MODE=standalone, que é o modo usado em staging e produção. Não há diferença de comportamento entre os modos: o building block não chama nenhum outro building block. O que muda é que, em standalone, applyCommonMiddleware é a única fonte de limite de corpo, CORS, cabeçalhos de segurança e limite de taxa.


08

Conceitos e modelo de dados

Glossário

Glossário

TermoSignifica
TemplateO documento com marcadores no lugar dos dados variáveis. Tem slug único dentro da organização e conteúdo em HTML com sintaxe Handlebars.
Rascunho de trabalhoO conteúdo corrente do template, em document_templates.content. É o que a pré-visualização mostra e o que a publicação copia. Não é usado em renderização.
VersãoCópia imutável do conteúdo e da declaração de variáveis, criada por POST /publish. Numerada a partir de 1. É o que a renderização usa.
VariávelCampo declarado com name, type, required, default e description. O nome precisa casar ^[a-zA-Z_][a-zA-Z0-9_]*$ e não pode repetir dentro do template.
Dados de exemplo (sampleData)Conjunto de valores usado pela pré-visualização quando nenhum valor é informado. Serve para o template ter uma aparência realista durante a edição.
Documento renderizadoLinha em rendered_documents com os valores de entrada, o HTML produzido, o número da versão usada e o vínculo com a operação.
businessIdIdentificador livre da operação de origem — proposta, contrato, pedido. É como você reencontra tudo que foi emitido para um caso.
CCBCédula de Crédito Bancário, título de crédito regido pelos arts. 26 a 45 da Lei nº 10.931/2004. É título executivo extrajudicial (art. 28) e tem seis requisitos essenciais de forma (art. 29). Motivou o desenho deste building block e acompanha o seed do projeto como exemplo.
Requisitos essenciais da CCBOs seis incisos do art. 29: a denominação "Cédula de Crédito Bancário"; a promessa de pagar; data e lugar do pagamento, com valores das prestações ou critérios; o nome da instituição credora; data e lugar da emissão; e a assinatura do emitente e do garantidor. O § 5º, incluído pela Lei nº 13.986/2020, admite assinatura eletrônica desde que garantida a identificação inequívoca do signatário. Este building block não valida esses requisitos — ver §15.
pdf-rendererServiço acompanhante que recebe HTML e devolve PDF. Não faz parte deste building block e não está publicado — ver §15.

Modelo de dados

Modelo de dados — schema doctemplates no PostgreSQL. 3 modelos.

Modelo PrismaTabelaPropósitoCampos-chave
DocumentTemplatedocument_templatesO template e seu rascunho de trabalhoslug (único por organização), category, status, content, variables, sampleData, currentVersionNumber, isActive, deletedAt
DocumentTemplateVersiondocument_template_versionsVersão publicada, imutávelÚnico (templateId, versionNumber), content, variables, changelog, publishedAt
RenderedDocumentrendered_documentsRegistro de cada emissãotemplateVersionNumber, inputData, outputFormat, outputHtml, fileStorageId, businessId, metadata
erDiagram
  Organization ||--o{ DocumentTemplate : possui
  Organization ||--o{ RenderedDocument : possui
  DocumentTemplate ||--o{ DocumentTemplateVersion : versiona
  DocumentTemplate ||--o{ RenderedDocument : origina
  Organization {
    uuid id PK
  }
  DocumentTemplate {
    uuid id PK
    uuid organizationId FK
    string slug
    enum category
    enum status
    text content
    jsonb variables
    jsonb sampleData
    int currentVersionNumber
    bool isActive
    datetime deletedAt
  }
  DocumentTemplateVersion {
    uuid id PK
    uuid templateId FK
    int versionNumber
    text content
    jsonb variables
    text changelog
    datetime publishedAt
  }
  RenderedDocument {
    uuid id PK
    uuid organizationId FK
    uuid templateId FK
    int templateVersionNumber
    jsonb inputData
    enum outputFormat
    text outputHtml
    uuid fileStorageId
    string businessId
    jsonb metadata
  }

Templates e documentos renderizados são isolados por organizationId, com chave estrangeira para a organização do IAM. Versões herdam o isolamento pelo template, e a exclusão do template remove as versões em cascata.

Enumerações

Enumerações

EnumValores
DocumentTemplateStatusDRAFT · PUBLISHED · ARCHIVED (este último declarado, nunca atribuído pelo código — ver §15)
DocumentTemplateCategoryCONTRACT · PROPOSAL · AGREEMENT · RECEIPT · REPORT · OTHER (padrão)
RenderOutputFormatHTML · PDF
Tipos de variávelSTRING · NUMBER · BOOLEAN · DATE · ARRAY · OBJECT

Ciclo de vida do template

Ciclo de vida do template

stateDiagram-v2
  [*] --> DRAFT : POST de criação do template
  DRAFT --> PUBLISHED : POST de publish · copia conteúdo e variáveis para uma versão imutável
  PUBLISHED --> PUBLISHED : POST de publish de novo · currentVersionNumber vira 2, 3, ...
  PUBLISHED --> PUBLISHED : PATCH altera o rascunho, NÃO a versão publicada
  DRAFT --> DRAFT : PATCH altera o rascunho
  DRAFT --> Excluido : DELETE · exclusão lógica em deletedAt
  PUBLISHED --> Excluido : DELETE · exclusão lógica em deletedAt
  Excluido --> [*]
  note right of DRAFT : currentVersionNumber é 0 e não renderiza — o render devolve 400
  note right of PUBLISHED : renderiza sempre usando a ÚLTIMA versão; as anteriores continuam intactas
  note right of Excluido : some das consultas; as versões e os documentos já emitidos continuam no banco

Atenção. ARCHIVED existe no enum e nunca é atribuído pelo código (§15).

Como um valor chega ao documento

Como um valor chega ao documento

flowchart TD
  A["Variáveis enviadas na requisição"] --> B["Merge com os valores padrão declarados NA VERSÃO publicada"]
  B --> C{"Toda variável obrigatória está presente?"}
  C -->|Não| C400["400"]
  C -->|Sim| D{"Tipo confere campo a campo?"}
  D -->|Falha| D400["400"]
  D -->|Ok| E["Handlebars aplica os valores ao conteúdo DA VERSÃO"]
  E --> F["HTML final"]
  F --> G["Gravado em rendered_documents"]
  G --> H["Devolvido ao chamador"]

Atenção. A regra: a versão publicada é a autoridade sobre conteúdo e sobre quais variáveis existem. O rascunho não participa da renderização.


09

Referência da API

Prefixo HTTP: /document-templates. Em monolito, a base é http://localhost:3000. Em standalone, a porta é 3019; em staging, https://document-template.bb.stg.catalisa.app.

Atenção ao prefixo. O caminho real repete o nome do módulo: o basePath do app é /document-templates e os routers são montados em /api/v1/document-templates. A rota completa é, portanto, /document-templates/api/v1/document-templates/.... Não é erro de digitação — é o caminho que o serviço responde.

Todas as 11 rotas exigem, sem exceção: authMiddleware (Bearer JWT do IAM), requirePermission e o middleware local requireOrganization, que devolve 403 quando o token não carrega organizationId.

flowchart LR
  B["basePath /document-templates"] --> M["Montagem em /api/v1/document-templates"]
  M --> T["templateRouter · 8 rotas de catálogo, versão e publicação"]
  M --> R["renderRouter · render, render/pdf e preview"]
  B --> H["/health · pública, fora da contagem das 11"]

Templates — /document-templates/api/v1/document-templates

MétodoRotaDescriçãoPermissão
POST/document-templates/api/v1/document-templatesCria template em DRAFT. Responde 201DOCUMENT_TEMPLATES_CREATE
GET/document-templates/api/v1/document-templatesLista templates, paginadoDOCUMENT_TEMPLATES_READ
GET/document-templates/api/v1/document-templates/:templateIdBusca o template (mostra o rascunho)DOCUMENT_TEMPLATES_READ
PATCH/document-templates/api/v1/document-templates/:templateIdAtualiza o rascunhoDOCUMENT_TEMPLATES_UPDATE
DELETE/document-templates/api/v1/document-templates/:templateIdExclusão lógica. Responde 204DOCUMENT_TEMPLATES_DELETE
POST/document-templates/api/v1/document-templates/:templateId/publishCongela o rascunho em uma versão novaDOCUMENT_TEMPLATES_PUBLISH
GET/document-templates/api/v1/document-templates/:templateId/versionsLista versões, paginadoDOCUMENT_TEMPLATES_READ
GET/document-templates/api/v1/document-templates/:templateId/versions/:versionNumberBusca uma versão específicaDOCUMENT_TEMPLATES_READ

Filtros aceitos em GET /document-templates: filter[status], filter[category], filter[active] (true/false). Paginação por page[number] e page[size].

Renderização — mesmo prefixo

MétodoRotaDescriçãoPermissão
POST/document-templates/api/v1/document-templates/:templateId/renderRenderiza em HTML e registra a emissãoDOCUMENT_TEMPLATES_RENDER
POST/document-templates/api/v1/document-templates/:templateId/render/pdfRenderiza em PDF (base64) e registra a emissãoDOCUMENT_TEMPLATES_RENDER
POST/document-templates/api/v1/document-templates/:templateId/previewPré-visualiza o rascunho, sem validar e sem gravarDOCUMENT_TEMPLATES_RENDER

Saúde

MétodoRotaDescrição
GET/document-templates/healthSonda de disponibilidade. Pública, não contabilizada nas 11 rotas

POST /document-templates/api/v1/document-templates

Cria o template em DRAFT. O corpo usa o envelope JSON:API — data.attributes é obrigatório nesta rota.

Request

json
{
  "data": {
    "type": "document-templates",
    "attributes": {
      "name": "CCB - Cédula de Crédito Bancário",
      "slug": "ccb-credito-pessoal",
      "description": "Cédula para operações de crédito pessoal",
      "category": "CONTRACT",
      "content": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1><p>Devedor: {{nome_cliente}} — CPF {{cpf_cliente}}</p><p>Valor: R$ {{valor_principal}}</p>",
      "variables": [
        { "name": "nome_cliente", "type": "STRING", "required": true, "description": "Nome completo do devedor" },
        { "name": "cpf_cliente", "type": "STRING", "required": true },
        { "name": "valor_principal", "type": "NUMBER", "required": true }
      ],
      "sampleData": { "nome_cliente": "Fulano de Tal", "cpf_cliente": "000.000.000-00", "valor_principal": 10000 }
    }
  }
}
{
  "data": {
    "type": "document-templates",
    "attributes": {
      "name": "CCB - Cédula de Crédito Bancário",
      "slug": "ccb-credito-pessoal",
      "description": "Cédula para operações de crédito pessoal",
      "category": "CONTRACT",
      "content": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1><p>Devedor: {{nome_cliente}} — CPF {{cpf_cliente}}</p><p>Valor: R$ {{valor_principal}}</p>",
      "variables": [
        { "name": "nome_cliente", "type": "STRING", "required": true, "description": "Nome completo do devedor" },
        { "name": "cpf_cliente", "type": "STRING", "required": true },
        { "name": "valor_principal", "type": "NUMBER", "required": true }
      ],
      "sampleData": { "nome_cliente": "Fulano de Tal", "cpf_cliente": "000.000.000-00", "valor_principal": 10000 }
    }
  }
}
CampoTipoObrigatórioDescrição
namestring (1–200)SimNome de exibição
slugstring (1–100)SimIdentificador legível. Somente [a-z0-9-]. Único por organização
descriptionstring (máx. 5000)Não
categoryenumNãoPadrão OTHER
contentstringSimHTML com marcadores Handlebars
variablesVariableDefinition[]SimPode ser lista vazia. Nomes únicos e no formato ^[a-zA-Z_][a-zA-Z0-9_]*$
sampleDataobjetoNãoValores usados pela pré-visualização
metadataobjetoNãoCampo livre

Resposta 201 — o template nasce com status: "DRAFT" e currentVersionNumber: 0.

Erros

StatusQuando
400Corpo reprovado no Zod; slug fora do formato; nome de variável inválido ou repetido
403Sem DOCUMENT_TEMPLATES_CREATE, ou token sem organizationId
409Já existe template com esse slug na organização

POST /document-templates/api/v1/document-templates/:templateId/publish

Congela o rascunho corrente em uma versão nova e coloca o template em PUBLISHED.

Request (corpo opcional)

json
{ "changelog": "Inclui cláusula de portabilidade de crédito" }
{ "changelog": "Inclui cláusula de portabilidade de crédito" }

O changelog é livre, até 1000 caracteres, e é a única descrição que vai existir da mudança. Preencha — é ele que responde "o que mudou da versão 2 para a 3" numa auditoria.

Resposta 200 — o template volta com status: "PUBLISHED" e currentVersionNumber incrementado.

Publicar de novo sem alterar nada cria uma versão idêntica com número novo. Não há verificação de mudança.


POST /document-templates/api/v1/document-templates/:templateId/render

Renderiza em HTML usando a última versão publicada e grava a emissão.

Request

json
{
  "variables": {
    "nome_cliente": "Maria Souza",
    "cpf_cliente": "123.456.789-00",
    "valor_principal": 15000
  },
  "businessId": "proposta-2026-000481",
  "metadata": { "origem": "esteira-consignado" }
}
{
  "variables": {
    "nome_cliente": "Maria Souza",
    "cpf_cliente": "123.456.789-00",
    "valor_principal": 15000
  },
  "businessId": "proposta-2026-000481",
  "metadata": { "origem": "esteira-consignado" }
}
CampoTipoObrigatórioDescrição
variablesobjetoSimValores das variáveis. Chaves precisam bater com os nomes declarados na versão
outputFormat"HTML"NãoNesta rota só aceita HTML. Para PDF, use a rota /render/pdf
businessIdstringNãoIdentificador da operação de origem
metadataobjetoNãoCampo livre, gravado junto com a emissão

Resposta 200

json
{
  "data": {
    "type": "rendered-documents",
    "id": "7c1f9b2e-0000-0000-0000-000000000000",
    "attributes": {
      "html": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>...",
      "templateId": "3a2b1c00-0000-0000-0000-000000000000",
      "templateVersionNumber": 3
    }
  }
}
{
  "data": {
    "type": "rendered-documents",
    "id": "7c1f9b2e-0000-0000-0000-000000000000",
    "attributes": {
      "html": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>...",
      "templateId": "3a2b1c00-0000-0000-0000-000000000000",
      "templateVersionNumber": 3
    }
  }
}

O templateVersionNumber da resposta é o que você guarda na sua operação. É ele que responde, depois, qual redação foi usada.

Erros

StatusCódigoQuando
400VALIDATIONTemplate ainda em DRAFTTemplate must be published before rendering
400VALIDATIONVariável obrigatória ausente — Required variable 'X' is missing
400VALIDATIONTipo errado — Variable 'X' must be a number
400VALIDATIONFalha na compilação do Handlebars — Template rendering failed: ...
404NOT_FOUNDTemplate inexistente, excluído, ou de outra organização
404NOT_FOUNDTemplate marcado como publicado mas sem nenhuma versão gravada

POST /document-templates/api/v1/document-templates/:templateId/render/pdf

Mesmo contrato de entrada da rota de HTML. Renderiza o HTML, envia ao serviço acompanhante e devolve o PDF em base64 no campo pdfBase64.

Resposta 200

json
{
  "data": {
    "type": "rendered-documents",
    "id": "9d4e2f11-0000-0000-0000-000000000000",
    "attributes": {
      "templateId": "3a2b1c00-0000-0000-0000-000000000000",
      "templateVersionNumber": 3,
      "outputFormat": "PDF",
      "pdfBase64": "JVBERi0xLjQKJcfsj6IK..."
    }
  }
}
{
  "data": {
    "type": "rendered-documents",
    "id": "9d4e2f11-0000-0000-0000-000000000000",
    "attributes": {
      "templateId": "3a2b1c00-0000-0000-0000-000000000000",
      "templateVersionNumber": 3,
      "outputFormat": "PDF",
      "pdfBase64": "JVBERi0xLjQKJcfsj6IK..."
    }
  }
}

Erros adicionais

StatusCódigoQuando
400VALIDATIONPDF rendering is not configured. Set PDF_RENDERER_URL environment variable. — é a resposta atual em staging e produção (§15)
500INTERNALPDF renderer service is unavailable — a variável está configurada mas o serviço não responde
500INTERNALO renderizador respondeu com erro ou estourou os 60 segundos

O PDF não é gravado: apenas o HTML de origem vai para rendered_documents. O campo fileStorageId existe na tabela e nunca é preenchido (§15) — guardar o binário é responsabilidade de quem chama.


POST /document-templates/api/v1/document-templates/:templateId/preview

Pré-visualiza o rascunho de trabalho. Não exige template publicado, não valida variáveis e não grava nada.

Request (corpo opcional)

json
{ "variables": { "nome_cliente": "Fulano de Tal" } }
{ "variables": { "nome_cliente": "Fulano de Tal" } }

Sem variables, usa o sampleData do template. Sem sampleData, renderiza com os valores padrão declarados e deixa o resto em branco.

Resposta 200

json
{ "data": { "type": "template-previews", "attributes": { "html": "<h1>...</h1>" } } }
{ "data": { "type": "template-previews", "attributes": { "html": "<h1>...</h1>" } } }

Use durante a escrita do template. Não use como ensaio de emissão: a pré-visualização passa em casos que a renderização recusa.


10

Início rápido

Do zero a um contrato emitido, com validação funcionando. Comandos escritos para staging; não executados nesta redação.

flowchart LR
  P1["1 · autenticar no IAM"] --> P2["2 · criar o template em DRAFT"]
  P2 --> P3["3 · tentar renderizar e ver a recusa"]
  P3 --> P4["4 · publicar"]
  P4 --> P5["5 · renderizar de verdade"]
  P5 --> P6["6 · omitir uma variável e ver a validação proteger"]

1. Autenticar no IAM

bash
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

BASE=https://document-template.bb.stg.catalisa.app/document-templates/api/v1/document-templates

echo "${TOKEN:0:12}…"
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

BASE=https://document-template.bb.stg.catalisa.app/document-templates/api/v1/document-templates

echo "${TOKEN:0:12}…"

Resposta esperada: os primeiros caracteres do JWT. Se sair vazio, a autenticação falhou e nada adiante vai funcionar.

texto
eyJhbGciOiJI…
eyJhbGciOiJI…

2. Criar o template

bash
TPL=$(curl -s -X POST "$BASE" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": {
      "type": "document-templates",
      "attributes": {
        "name": "Recibo de Liberação",
        "slug": "recibo-liberacao-teste",
        "category": "RECEIPT",
        "content": "<h1>Recibo</h1><p>Recebi de {{instituicao}} a quantia de R$ {{valor}}.</p><p>{{nome_cliente}} — CPF {{cpf_cliente}}</p>",
        "variables": [
          { "name": "instituicao", "type": "STRING", "required": true },
          { "name": "valor", "type": "NUMBER", "required": true },
          { "name": "nome_cliente", "type": "STRING", "required": true },
          { "name": "cpf_cliente", "type": "STRING", "required": true }
        ]
      }
    }
  }')

TPL_ID=$(echo "$TPL" | jq -r '.data.id')
echo "$TPL" | jq '.data.attributes | {status, currentVersionNumber}'
TPL=$(curl -s -X POST "$BASE" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": {
      "type": "document-templates",
      "attributes": {
        "name": "Recibo de Liberação",
        "slug": "recibo-liberacao-teste",
        "category": "RECEIPT",
        "content": "<h1>Recibo</h1><p>Recebi de {{instituicao}} a quantia de R$ {{valor}}.</p><p>{{nome_cliente}} — CPF {{cpf_cliente}}</p>",
        "variables": [
          { "name": "instituicao", "type": "STRING", "required": true },
          { "name": "valor", "type": "NUMBER", "required": true },
          { "name": "nome_cliente", "type": "STRING", "required": true },
          { "name": "cpf_cliente", "type": "STRING", "required": true }
        ]
      }
    }
  }')

TPL_ID=$(echo "$TPL" | jq -r '.data.id')
echo "$TPL" | jq '.data.attributes | {status, currentVersionNumber}'

Resposta esperada: {"status": "DRAFT", "currentVersionNumber": 0}.

3. Tentar renderizar antes de publicar — e ver a recusa

bash
curl -s -X POST "$BASE/$TPL_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"variables":{"instituicao":"Financeira Exemplo","valor":15000,"nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jq
curl -s -X POST "$BASE/$TPL_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"variables":{"instituicao":"Financeira Exemplo","valor":15000,"nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jq
json
{ "error": "VALIDATION", "message": "Template must be published before rendering" }
{ "error": "VALIDATION", "message": "Template must be published before rendering" }

4. Publicar

bash
curl -s -X POST "$BASE/$TPL_ID/publish" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"changelog":"Versão inicial"}' | jq '.data.attributes | {status, currentVersionNumber}'
curl -s -X POST "$BASE/$TPL_ID/publish" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"changelog":"Versão inicial"}' | jq '.data.attributes | {status, currentVersionNumber}'

Resposta esperada: {"status": "PUBLISHED", "currentVersionNumber": 1}.

5. Renderizar de verdade

bash
curl -s -X POST "$BASE/$TPL_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "instituicao": "Financeira Exemplo",
      "valor": 15000,
      "nome_cliente": "Maria Souza",
      "cpf_cliente": "123.456.789-00"
    },
    "businessId": "proposta-teste-001"
  }' | jq '.data.attributes | {templateVersionNumber, html}'
curl -s -X POST "$BASE/$TPL_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "instituicao": "Financeira Exemplo",
      "valor": 15000,
      "nome_cliente": "Maria Souza",
      "cpf_cliente": "123.456.789-00"
    },
    "businessId": "proposta-teste-001"
  }' | jq '.data.attributes | {templateVersionNumber, html}'

Resposta esperada: a versão usada e o HTML já com os valores aplicados.

json
{
  "templateVersionNumber": 1,
  "html": "<h1>Recibo</h1><p>Recebi de Financeira Exemplo a quantia de R$ 15000.</p><p>Maria Souza — CPF 123.456.789-00</p>"
}
{
  "templateVersionNumber": 1,
  "html": "<h1>Recibo</h1><p>Recebi de Financeira Exemplo a quantia de R$ 15000.</p><p>Maria Souza — CPF 123.456.789-00</p>"
}

6. Confirmar que a validação protege o documento

bash
curl -s -X POST "$BASE/$TPL_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"variables":{"instituicao":"Financeira Exemplo","nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jq
curl -s -X POST "$BASE/$TPL_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"variables":{"instituicao":"Financeira Exemplo","nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jq
json
{ "error": "VALIDATION", "message": "Required variable 'valor' is missing" }
{ "error": "VALIDATION", "message": "Required variable 'valor' is missing" }

Nenhum documento foi gerado e nenhuma linha foi gravada. É esse comportamento que impede contrato com campo em branco de chegar ao cliente.

Credenciais de staging, conforme AMBIENTES.md. Nunca use credencial de produção em documentação ou script de exemplo.


11

Receitas

Emitir uma CCB a partir do resultado da esteira de crédito

O caso central. O calculations-engine produz os números; o Document Template produz o documento.

flowchart LR
  CE["Calculations Engine · parcela, IOF, CET, cronograma"] --> V["Variáveis da CCB"]
  CU["Customers · nome, CPF"] --> V
  V --> R["POST de render no template da CCB"]
  R --> D["CCB emitida, com templateVersionNumber e id"]

1. Enviar os valores da esteira e emitir a cédula

bash
# 1. Os valores vêm da sua esteira — aqui, ilustrados
curl -s -X POST "$BASE/$CCB_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "instituicao": "Financeira Exemplo S.A.",
      "cnpj_instituicao": "00.000.000/0001-00",
      "nome_cliente": "Maria Souza",
      "cpf_cliente": "123.456.789-00",
      "valor_principal": 15000,
      "taxa_mensal": 2.19,
      "taxa_anual": 29.68,
      "cet_anual": 34.12,
      "num_parcelas": 36,
      "valor_parcela": 612.44,
      "data_emissao": "2026-08-16",
      "data_vencimento_primeira": "2026-09-16",
      "sistema_amortizacao": "PRICE",
      "garantias": "Sem garantia real",
      "iof_total": 412.55
    },
    "businessId": "proposta-2026-000481"
  }' | jq '{versao: .data.attributes.templateVersionNumber, id: .data.id}'
# 1. Os valores vêm da sua esteira — aqui, ilustrados
curl -s -X POST "$BASE/$CCB_ID/render" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "instituicao": "Financeira Exemplo S.A.",
      "cnpj_instituicao": "00.000.000/0001-00",
      "nome_cliente": "Maria Souza",
      "cpf_cliente": "123.456.789-00",
      "valor_principal": 15000,
      "taxa_mensal": 2.19,
      "taxa_anual": 29.68,
      "cet_anual": 34.12,
      "num_parcelas": 36,
      "valor_parcela": 612.44,
      "data_emissao": "2026-08-16",
      "data_vencimento_primeira": "2026-09-16",
      "sistema_amortizacao": "PRICE",
      "garantias": "Sem garantia real",
      "iof_total": 412.55
    },
    "businessId": "proposta-2026-000481"
  }' | jq '{versao: .data.attributes.templateVersionNumber, id: .data.id}'

Resposta esperada: a versão que gerou o documento e o identificador da emissão.

json
{ "versao": 3, "id": "7c1f9b2e-0000-0000-0000-000000000000" }
{ "versao": 3, "id": "7c1f9b2e-0000-0000-0000-000000000000" }

Armadilhas.

  • Guarde o templateVersionNumber e o id do documento renderizado na sua operação, não só aqui. São eles que ligam a operação à redação usada.
  • Valores monetários são NUMBER. Se a sua esteira manda "15000,00" como texto, a validação recusa com Variable 'valor_principal' must be a number. Converta antes.
  • Datas aceitam Date ou texto ISO. "16/08/2026" é reprovado — use "2026-08-16".
  • Formatação de moeda é responsabilidade do template. O Handlebars não formata número: {{valor_principal}} imprime 15000, não 15.000,00. Envie o valor já formatado numa variável de texto separada se a apresentação importar.
  • O seed do projeto (prisma/seed-from-services.ts) traz uma CCB completa com quinze variáveis e uma segunda versão com cláusula de portabilidade. É o melhor ponto de partida para escrever a sua.

Mudar a redação de um contrato sem afetar o que já está em uso

Alterar o texto vigente sem que nenhuma emissão em andamento mude de comportamento antes da hora.

flowchart LR
  S1["1 · PATCH altera o rascunho"] --> S2["2 · preview confere o resultado"]
  S2 --> S3["3 · publish cria a versão nova"]
  S1 -.->|"até o passo 3, o render usa a versão anterior"| S3

1. Alterar o rascunho — não afeta nenhuma renderização ainda

bash
# 1. Alterar o rascunho — não afeta nenhuma renderização ainda
curl -s -X PATCH "$BASE/$TPL_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"content":"<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>... nova redação ..."}' | jq '.data.attributes.currentVersionNumber'
# 1. Alterar o rascunho — não afeta nenhuma renderização ainda
curl -s -X PATCH "$BASE/$TPL_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"content":"<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>... nova redação ..."}' | jq '.data.attributes.currentVersionNumber'

Resposta esperada: o número da versão não muda — o PATCH mexe no rascunho, não na versão publicada.

json
2
2

2. Conferir o resultado antes de publicar

bash
# 2. Conferir o resultado antes de publicar
curl -s -X POST "$BASE/$TPL_ID/preview" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' \
  | jq -r '.data.attributes.html' > /tmp/previa.html
# 2. Conferir o resultado antes de publicar
curl -s -X POST "$BASE/$TPL_ID/preview" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' \
  | jq -r '.data.attributes.html' > /tmp/previa.html

Resposta esperada: nenhuma saída no terminal e o arquivo gravado com o rascunho já renderizado.

bash
$ head -c 60 /tmp/previa.html
<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>
$ head -c 60 /tmp/previa.html
<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>

3. Publicar quando estiver certo

bash
# 3. Publicar quando estiver certo
curl -s -X POST "$BASE/$TPL_ID/publish" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"changelog":"Ajuste na cláusula de vencimento antecipado — parecer jurídico 2026-08"}' | jq
# 3. Publicar quando estiver certo
curl -s -X POST "$BASE/$TPL_ID/publish" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"changelog":"Ajuste na cláusula de vencimento antecipado — parecer jurídico 2026-08"}' | jq

Resposta esperada: agora sim o número da versão sobe, e é essa versão que passa a ser usada nas próximas renderizações.

json
{ "data": { "type": "document-templates", "attributes": { "status": "PUBLISHED", "currentVersionNumber": 3 } } }
{ "data": { "type": "document-templates", "attributes": { "status": "PUBLISHED", "currentVersionNumber": 3 } } }

Armadilhas.

  • Entre o passo 1 e o passo 3, as renderizações continuam usando a versão anterior. Isso é o comportamento desejado, mas surpreende quem espera que o PATCH já valha.
  • GET no template mostra o rascunho, não o que está sendo emitido. Para ver o que está no ar, consulte GET /:id/versions/:n com o currentVersionNumber.
  • Se você adicionar uma variável obrigatória nova, a publicação passa a quebrar as chamadas de renderização que ainda não enviam esse campo. Publique a mudança de contrato de dados junto com o deploy de quem chama, ou declare a variável com default.
  • Não há verificação de mudança: publicar sem alterar nada cria uma versão idêntica.

Reproduzir um documento emitido meses atrás

Recuperar a redação exata que gerou um documento antigo, a partir do número da versão que você guardou na operação.

1. Buscar a versão pelo número guardado

bash
# Você guardou o número da versão na sua operação
curl -s "$BASE/$TPL_ID/versions/3" -H "Authorization: Bearer $TOKEN" \
  | jq '{versao: .data.attributes.versionNumber,
         publicada_em: .data.attributes.publishedAt,
         changelog: .data.attributes.changelog,
         variaveis: [.data.attributes.variables[].name]}'
# Você guardou o número da versão na sua operação
curl -s "$BASE/$TPL_ID/versions/3" -H "Authorization: Bearer $TOKEN" \
  | jq '{versao: .data.attributes.versionNumber,
         publicada_em: .data.attributes.publishedAt,
         changelog: .data.attributes.changelog,
         variaveis: [.data.attributes.variables[].name]}'

Resposta esperada: a versão, a data de publicação, o changelog e os nomes das variáveis que existiam naquela época.

json
{
  "versao": 3,
  "publicada_em": "2026-03-11T14:22:07.512Z",
  "changelog": "Inclui cláusula de portabilidade de crédito",
  "variaveis": ["nome_cliente", "cpf_cliente", "valor_principal", "iof_total"]
}
{
  "versao": 3,
  "publicada_em": "2026-03-11T14:22:07.512Z",
  "changelog": "Inclui cláusula de portabilidade de crédito",
  "variaveis": ["nome_cliente", "cpf_cliente", "valor_principal", "iof_total"]
}

Armadilhas.

  • A versão devolve o conteúdo e a declaração de variáveis daquela época. Os valores usados na emissão estão em rendered_documents.inputData — e hoje não há endpoint que leia essa tabela (§15). Para uma auditoria, o acesso é por consulta direta ao banco.
  • Renderizar de novo com a versão antiga não é possível pela API: a renderização sempre usa a última versão publicada. Reproduzir exige recuperar o HTML gravado.

Montar uma tabela de parcelas dentro do documento

Handlebars é uma engine sem lógica, mas tem #each. Declare a lista como ARRAY:

html
<table>
  <tr><th>Parcela</th><th>Vencimento</th><th>Valor</th></tr>
  {{#each parcelas}}
  <tr><td>{{this.numero}}</td><td>{{this.vencimento}}</td><td>R$ {{this.valor}}</td></tr>
  {{/each}}
</table>
<table>
  <tr><th>Parcela</th><th>Vencimento</th><th>Valor</th></tr>
  {{#each parcelas}}
  <tr><td>{{this.numero}}</td><td>{{this.vencimento}}</td><td>R$ {{this.valor}}</td></tr>
  {{/each}}
</table>
json
{ "name": "parcelas", "type": "ARRAY", "required": true }
{ "name": "parcelas", "type": "ARRAY", "required": true }

Armadilhas.

  • A validação confere que parcelas é um array. Não confere o que tem dentro — objeto com a chave errada some do documento em silêncio. Valide a forma dos itens antes de enviar.
  • {{this.valor}} sai sem formatação. Formate os números na sua aplicação antes de montar o array.
  • O extrator de variáveis do template (extractTemplateVariables) ignora #each e #if, mas ele só é usado como utilitário — a declaração de variáveis continua sendo manual e é ela que vale.

Distinguir "não configurado" de "fora do ar" na geração de PDF

Saber, pela resposta, se o problema é falta de infraestrutura ou falha do renderizador — porque o tratamento é diferente.

1. Chamar a rota de PDF e olhar só o par erro e mensagem

bash
curl -s -X POST "$BASE/$TPL_ID/render/pdf" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"variables":{ ... }}' | jq '{error, message}'
curl -s -X POST "$BASE/$TPL_ID/render/pdf" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"variables":{ ... }}' | jq '{error, message}'

Resposta esperada hoje, em staging e produção:

json
{ "error": "VALIDATION", "message": "PDF rendering is not configured. Set PDF_RENDERER_URL environment variable." }
{ "error": "VALIDATION", "message": "PDF rendering is not configured. Set PDF_RENDERER_URL environment variable." }

2. Ler a resposta na tabela

RespostaSignificaO que fazer
400 · PDF rendering is not configuredPDF_RENDERER_URL não está definida no ambienteÉ o estado atual de staging e produção (§15). Use a saída em HTML
500 · PDF renderer service is unavailableA variável existe, o serviço não respondeVerifique o serviço acompanhante e a rede
500 · PDF renderer returned 5xxO serviço respondeu com erroVerifique o HTML de entrada e os logs do renderizador

Armadilha. A distinção importa: o 400 não é problema seu de integração, é ausência de infraestrutura. Não trate como erro de requisição nem tente repetir.


12

Integração com outros building blocks

O Document Template é a primeira peça da cadeia de documentos da Catalisa. Ele gera; o e-signature coleta a assinatura; o data-extraction lê o documento que volta preenchido ou o comprovante que o cliente envia.

Building blockComo se relacionaObrigatório
IAMEmite o token e define o organizationId que isola templates e documentosSim
E-SignatureRecebe o documento gerado e coleta as assinaturasNão
File StorageGuarda o PDF gerado — hoje a ligação é manual (§15)Não
Calculations EngineProduz os números da CCB: parcela, IOF, CET, cronogramaNão
Decision PlatformOrquestra a esteira que decide, calcula e manda emitir o documentoNão
CustomersFornece os dados cadastrais do devedor que preenchem o documentoNão
Webhooks EngineEntrega document-templates.document.rendered a sistemas externosNão
Audit TrailRegistra quem publicou versão e quem emitiu documentoNão

A cadeia de documentos, ponta a ponta

A cadeia de documentos, ponta a ponta

flowchart TD
  CU["Customers"] -->|"dados cadastrais"| DT
  CE["Calculations Engine"] -->|"parcela, IOF, CET"| DT
  DP["Decision Platform"] -->|"decide e orquestra"| DT
  DT["Document Template · gera a CCB a partir da versão publicada"]
  DT -->|"HTML ou PDF"| ES["E-Signature · coleta assinatura dos signatários por e-mail"]
  ES -->|"documento assinado"| FS["File Storage · guarda o assinado"]
  ES -->|"documento assinado"| WE["Webhooks Engine · avisa o sistema do cliente"]

E na volta, quando o cliente manda um comprovante ou um documento preenchido:

flowchart LR
  FS2["File Storage"] -->|"arquivo"| DE["Data Extraction · lê e devolve JSON"]
  DE -->|"dados estruturados"| EST["De volta à esteira"]

Gerar a CCB aqui e mandar assinar no e-signature

O par mais forte da cadeia é este: o Document Template emite a cédula, o e-signature coleta as assinaturas. A ligação entre os dois é o fileId do file-storage — o SignatureDocument guarda esse campo e o serviço de assinatura baixa o arquivo por ele antes de mandar ao provedor.

O passo de upload é manual hoje: o Document Template devolve o PDF em base64 e não grava nada no file-storage, porque fileStorageId nunca é preenchido (§15). Quem orquestra a esteira faz o upload e passa o fileId adiante.

sequenceDiagram
  autonumber
  participant App as Aplicação · esteira de crédito
  participant DT as Document Template
  participant PR as pdf-renderer acompanhante
  participant FS as File Storage
  participant ES as E-Signature
  participant PV as Provedor de assinatura
  App->>DT: "POST de render/pdf no template da CCB · variáveis e businessId"
  DT->>PR: "HTML compilado da versão publicada"
  PR-->>DT: "Binário do PDF"
  DT-->>App: "200 · pdfBase64 e templateVersionNumber"
  App->>FS: "POST /file-storage/api/v1/files com o PDF"
  FS-->>App: "fileId"
  App->>ES: "POST /e-signature/api/v1/esignature/requests · fileId, signatários e businessId"
  ES->>FS: "Gera URL de download e baixa o arquivo do fileId"
  ES->>PV: "Cria o envelope com o documento e os signatários"
  PV-->>ES: "viewUrl e id externo"
  ES-->>App: "201 · id da solicitação e viewUrl"
  PV->>ES: "Webhook de assinado ou concluído"
  App->>ES: "GET /e-signature/api/v1/esignature/requests/:id/download"
  ES-->>App: "URL do documento assinado, com validade"

Atenção. O e-signature aceita fileId ou fileBase64 na criação da solicitação; um dos dois é obrigatório. Passar o base64 direto pula o file-storage, mas também abre mão de ter o documento original guardado com o resto da operação.

Este diagrama é o argumento comercial: gerar, assinar e ler são o mesmo problema visto de três ângulos, e na Catalisa são três peças que já compartilham o token, o tenant e o businessId. Quem monta essa cadeia com três fornecedores diferentes gasta a maior parte do esforço em amarrar identidade, isolamento e correlação entre eles — trabalho que aqui não existe porque as peças já nasceram no mesmo catálogo.

O businessId é o fio que costura tudo: o mesmo identificador de proposta vai no documento gerado, na solicitação de assinatura e na extração do comprovante. Listar tudo que aconteceu com uma operação é filtrar por um campo em três serviços.


13

Configuração e operação

Variáveis de ambiente

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
DATABASE_URLPostgreSQL. O building block usa o schema doctemplatesSim
REDIS_URLRedis, usado pelo limite de taxa e pelo publicador de eventosSim
JWT_SECRETSegredo HS256 compartilhado com o IAM. Mínimo 44 caracteresSim
PDF_RENDERER_URLURL do serviço acompanhante de PDF. Sem ela, a rota de PDF devolve 400Nãohttp://pdf-renderer:3020 no cliente, mas o PDFClient só é registrado se a variável existir
PORTPorta em standaloneNão3000 (a topologia expõe 3019)
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

O PDFClient só entra no container de dependências quando PDF_RENDERER_URL está definida. Se ela não estiver, o serviço sobe normalmente e apenas a rota de PDF fica indisponível — o que é o comportamento certo, mas silencioso. Verifique na subida.

Dependências de infraestrutura

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema doctemplates — templates, versões e documentos emitidos
RedisContadores de limite de taxa e publicação de eventos
IAMVerificação do token; o organizationId vem do claim assinado
pdf-rendererServiço acompanhante que converte HTML em PDF. Opcional e hoje não publicado (§15)

Limites e quotas

Limites e quotas

LimiteValorOnde
Corpo da requisição1 MBapplyCommonMiddleware — vale para o content do template e para o corpo de renderização
Nome do template200 caracteresZod
slug100 caracteres, [a-z0-9-]Zod e serviço
Descrição5.000 caracteresZod
changelog1.000 caracteresZod
Espera pelo renderizador de PDF60 segundosPDFClient
Limite de taxa global10.000 requisições por minuto por IP, quando ligadoRATE_LIMIT_GLOBAL_MAX

Catálogo de erros

Catálogo de erros

O envelope de erro é { "error": "<código>", "message": "<texto>", "details": <opcional> }.

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado, variável faltando, tipo errado, template não publicado, ou PDF não configuradoLeia a message: ela nomeia a variável ou a condição
401Token ausente, inválido ou expiradoRenove no IAM
403Falta a permissão exigida, ou o token não carrega organizationIdConfira o papel e a organização no token
404NOT_FOUNDTemplate, versão ou recurso inexistente, excluído, ou de outra organizaçãoConfira o identificador. Note que "de outra organização" e "inexistente" são a mesma resposta, de propósito
409CONFLICTslug já usado na organizaçãoEscolha outro
413Corpo acima de 1 MBReduza o template ou o volume de variáveis
429Limite de taxa estouradoAplique recuo exponencial
500INTERNALFalha no banco, ou o renderizador de PDF indisponívelVerifique a message e os logs

Observabilidade

Observabilidade.

  • GET /document-templates/health responde a sonda de disponibilidade. É a rota usada pelo healthcheck da pilha em staging e produção, via HEALTH_BASE_PATH=document-templates.
  • Cada renderização publica document-templates.document.rendered com o identificador do documento, o do template, o número da versão e o businessId. É a métrica de volume mais confiável, porque só é emitida depois de a linha ser gravada.
  • Falhas de PDF são registradas pelo PDFClient com a mensagem original do renderizador antes de virarem INTERNAL.
  • Não há métrica de latência de renderização exposta. Para medir, use o intervalo entre a requisição e o evento.

14

Segurança e compliance

Isolamento entre tenants

Isolamento entre tenants. O organizationId vem do claim assinado do JWT e nunca do corpo. As três rotas de renderização e as oito de template passam pelo middleware local requireOrganization, que devolve 403 quando o claim está ausente.

flowchart TD
  A["Requisição com Bearer JWT"] --> B["authMiddleware · verifica a assinatura"]
  B --> C["requirePermission · confere a permissão exigida"]
  C --> D{"O token carrega organizationId?"}
  D -->|Não| E["403 · Organization context required"]
  D -->|Sim| F["Repositório · WHERE organizationId e deletedAt nulo"]
  F --> G{"Achou dentro da organização?"}
  G -->|Não| H["404 · mesma resposta para inexistente e de outro tenant"]
  G -->|Sim| I["Segue o fluxo"]

Toda consulta de repositório inclui organizationId na cláusula WHEREfindById, findBySlug e findMany filtram por organização e por deletedAt: null. Um template de outra organização responde 404, não 403: a distinção permitiria descobrir a existência do recurso.

O que este building block guarda de dado pessoal

O que este building block guarda de dado pessoal. Esta é a parte que precisa estar clara para quem avalia LGPD.

OndeO que fica gravadoPor quanto tempo
rendered_documents.inputDataTodos os valores enviados na renderização — nome, CPF, valor, condições de créditoIndefinidamente. Não há política de retenção nem expurgo automático
rendered_documents.outputHtmlO documento inteiro, com os dados já aplicadosIndefinidamente
document_templates.sampleDataValores de exemplo, que muitas vezes são copiados de um caso realIndefinidamente
document_template_versions.contentO texto do contrato. Não contém dado de clienteIndefinidamente

Em outras palavras: a tabela de documentos emitidos é um repositório de contratos com dado pessoal, e é por isso que ela existe — sem ela não há reprodutibilidade. Trate-a com o mesmo cuidado do banco de clientes: controle de acesso ao banco, cifragem em repouso no volume, e um processo definido para atender pedido de eliminação. Hoje esse expurgo é manual (§15).

Atenção. Evite colocar dado real em sampleData; ele é retornado em toda leitura do template, para quem tiver apenas DOCUMENT_TEMPLATES_READ.

O conteúdo não sai da sua infraestrutura

O conteúdo não sai da sua infraestrutura. A compilação do Handlebars acontece dentro do processo. A única saída é a chamada ao serviço acompanhante de PDF, e mesmo essa é uma URL interna que você controla. Não há chamada a serviço de terceiro em nenhum caminho — o que é a diferença material frente a um conversor de PDF em nuvem, onde o contrato com CPF e condições de crédito trafega para fora.

Escapamento de conteúdo

Escapamento de conteúdo. O Handlebars escapa HTML em {{variavel}} por padrão. Um valor vindo do cliente com <script> sai como texto, não como marcação. A forma {{{variavel}}} desliga o escapamento e deve ser reservada para conteúdo que a sua aplicação montou e conferiu.

O template é código que você executa

O template é código que você executa. Quem tem DOCUMENT_TEMPLATES_CREATE ou _UPDATE escreve HTML que será compilado pelo serviço e, na rota de PDF, carregado por um renderizador. Trate essas duas permissões como privilégio elevado e conceda-as a poucas contas — o mesmo critério que você usaria para quem pode alterar uma regra de negócio em produção.

Permissões exigidas

Permissões exigidas. Todas as rotas exigem token válido do IAM mais a permissão específica. As seis permissões (CREATE, READ, UPDATE, DELETE, PUBLISH, RENDER) permitem separar quem escreve o template de quem publica e de quem emite — a separação que uma operação regulada normalmente exige entre jurídico, aprovador e esteira. Note que, hoje, apenas o papel ADMIN recebe essas permissões no catálogo padrão do IAM; papéis intermediários precisam ser configurados (§15).

PermissãoHabilitaSeparação que ela permite
DOCUMENT_TEMPLATES_CREATECriar templateQuem escreve a minuta
DOCUMENT_TEMPLATES_READLer template e versõesQuem audita e quem consulta a redação vigente
DOCUMENT_TEMPLATES_UPDATEAlterar o rascunhoQuem revisa o texto jurídico
DOCUMENT_TEMPLATES_DELETEExcluir logicamenteQuem administra o catálogo
DOCUMENT_TEMPLATES_PUBLISHCongelar o rascunho em versãoO aprovador — o papel que a operação regulada separa do autor
DOCUMENT_TEMPLATES_RENDERRenderizar e pré-visualizarA esteira que emite

Exclusão lógica

Exclusão lógica. Templates usam deletedAt. Excluir um template não apaga as versões publicadas nem os documentos já emitidos — o histórico sobrevive à remoção, que é o que auditoria e obrigação de retenção exigem.

Enquadramento regulatório da CCB

Enquadramento. Para documentos de crédito, a CCB é regida pelos arts. 26 a 45 da Lei nº 10.931/2004. A lei admite expressamente a forma eletrônica: o art. 29, § 5º, incluído pela Lei nº 13.986/2020, estabelece que a assinatura do emitente "poderá ocorrer sob a forma eletrônica, desde que garantida a identificação inequívoca de seu signatário".

Duas consequências práticas, e é importante que elas fiquem separadas:

  1. A validação genérica de variável não é validação jurídica. O building block garante que toda variável declarada como obrigatória chegou preenchida. Ele não sabe o que são os seis requisitos essenciais do art. 29 e não confere se o seu template os contempla. Um {{data_pagamento}} que você esqueceu de declarar como obrigatória sai vazio, e um requisito essencial em branco compromete a força executiva do título. A conferência da minuta é responsabilidade do seu jurídico — e vale ter um validador de requisitos essenciais na sua esteira, antes de chamar a renderização.
  2. A identificação inequívoca do signatário não acontece aqui. Ela é assunto do e-signature, e a §14 dele explica com precisão o que é e o que não é garantido hoje. Este building block gera o documento; ele não assina, não autentica signatário e não produz prova de autoria.

15

Limitações conhecidas

LimitaçãoImpactoSituação
O serviço de PDF não está publicadoPDF_RENDERER_URL não está definida em staging nem em produção, e não existe imagem, docker-compose ou manifesto do pdf-renderer no repositório. A rota /render/pdf devolve 400 nos ambientes publicados.Bloqueante para PDF — só a saída em HTML funciona hoje
Não há endpoint para ler documentos emitidosrendered_documents é gravada em toda renderização e nenhuma rota a expõe. Recuperar um documento emitido exige consulta direta ao bancoRoadmap — é a lacuna mais sentida em auditoria
fileStorageId nunca é preenchidoA coluna existe na tabela e a integração com o File Storage não está implementada. O PDF volta em base64 e some se você não guardarRoadmap
ARCHIVED nunca é atribuídoO enum de status tem o valor, mas nenhuma rota o define. Arquivar template é, na prática, excluir logicamenteEspecificado, não implementado
Renderização sempre usa a última versão publicadaNão há como pedir emissão com uma versão específica. Reproduzir um documento antigo exige recuperar o HTML gravadoPor design, mas limita reemissão
A pré-visualização não valida variáveisPOST /preview passa em casos que POST /render recusa. Usar a pré-visualização como ensaio dá falsa confiançaPor design — a pré-visualização existe para edição
Publicar não verifica se houve mudançaChamadas repetidas criam versões idênticas com números diferentes, poluindo o históricoRoadmap
Corpo limitado a 1 MBTemplates grandes de contrato, com CSS embutido e imagens em base64, batem no teto e recebem 413Por design do middleware comum; ajustável na infraestrutura
Sem saída em DOCX ou ODTSó HTML e PDF. Quem precisa entregar arquivo editável ao jurídico não é atendidoFora de escopo
Sem editor visualO template é HTML escrito à mão. O jurídico depende de alguém que escreva HTMLPor design — é uma API
Sem retenção nem expurgo automatizadoDocumentos emitidos com dado pessoal ficam indefinidamente. Atender pedido de eliminação da LGPD é processo manualRoadmap
Sem numeração ou identificador de documento no conteúdoO building block não gera número sequencial de contrato nem carimbo. Se o seu documento precisa disso, passe como variávelFora de escopo
Não valida requisitos essenciais da CCBA validação é genérica — presença e tipo das variáveis declaradas. O serviço não conhece os seis incisos do art. 29 da Lei nº 10.931/2004 e não avisa se o seu template deixou um de foraFora de escopo hoje; um validador de requisitos por categoria de documento está no roadmap
Sem hash nem carimbo de integridade do documento emitidorendered_documents guarda o HTML, mas não um resumo criptográfico que prove que a linha não foi alterada depois. Para documento financeiro, integridade verificável costuma ser exigênciaRoadmap
Permissões só no papel ADMINDOCUMENT_TEMPLATES_* não está atribuída a nenhum papel intermediário no catálogo padrão do IAM. O papel DOCUMENT_OPERATOR recebe PDF_CCB_CREATE e PDF_TEMPLATE_RENDER, que nenhuma rota deste building block exige — são resquício de uma versão anteriorConfiguração pendente no IAM
Sem formatação de número e data na engineHandlebars não formata. {{valor}} imprime 15000, não 15.000,00. A formatação é responsabilidade de quem chamaPor design; auxiliares de formatação estão no roadmap
Sem cabeçalho e rodapé recorrentesA paginação depende inteiramente do renderizador acompanhante e do CSS que você escreverDepende da escolha do renderizador

16

Perguntas frequentes

Consigo gerar PDF hoje?

Localmente sim, se você subir um serviço que aceite POST /api/v1/render-pdf com { html, options } e devolva o binário, e apontar PDF_RENDERER_URL para ele. Em staging e produção, não: a variável não está configurada e o serviço não está publicado. A rota responde 400 com PDF rendering is not configured. Trate a saída em HTML como o caminho suportado hoje e leia a §15 antes de prometer PDF em proposta.

Por que a rota é /document-templates/api/v1/document-templates?

Porque o basePath do app e o ponto de montagem dos routers repetem o nome do módulo. É feio e é real — o serviço responde nesse caminho. Copie do §9 em vez de deduzir.

Editar um template publicado quebra os contratos já emitidos?

Não. A edição altera o rascunho de trabalho. As versões publicadas são imutáveis e os documentos já emitidos não mudam. A alteração só passa a valer para novas renderizações depois de POST /publish — e mesmo assim, cada documento antigo continua apontando para a versão que o gerou.

Como sei qual redação um cliente recebeu?

Pelo templateVersionNumber que a renderização devolve. Guarde-o na sua operação. Depois, GET /:templateId/versions/:n traz o conteúdo exato daquela versão, a data de publicação e o changelog. Os valores usados estão em rendered_documents.inputData, hoje acessível apenas por consulta ao banco (§15).

Posso usar isto para gerar CCB de verdade?

A lei permite a forma eletrônica: o art. 29, § 5º da Lei nº 10.931/2004, incluído pela Lei nº 13.986/2020, admite assinatura eletrônica na cédula desde que garantida a identificação inequívoca do signatário. O building block gera o documento com a redação que você escrever e valida que todos os campos declarados chegaram — mas ele não confere os seis requisitos essenciais do art. 29, não numera, não registra em nenhuma central e não assina. A conformidade da redação é do seu jurídico; a identificação do signatário é do e-signature, e a §14 dele explica exatamente que nível de assinatura é produzido hoje. O seed do projeto traz uma CCB de exemplo com quinze variáveis, útil como ponto de partida — não como modelo jurídico aprovado.

O template pode ter lógica condicional?

Sim, dentro do que o Handlebars oferece: {{#if}}, {{#unless}}, {{#each}} e {{#with}}. Não há auxiliares personalizados registrados, então não há formatação de moeda, de data nem cálculo dentro do template. Se o documento precisa de um valor formatado, mande-o já formatado como STRING.

O que acontece se eu adicionar uma variável obrigatória a um template em uso?

Nada até você publicar. Depois de publicar, toda renderização que não enviar o campo novo passa a receber 400. Coordene a publicação com o deploy de quem chama, ou declare a variável com um default para que a transição seja suave.

Os dados do contrato saem da minha infraestrutura?

Não. A renderização acontece no processo, e a única chamada externa é ao renderizador de PDF, que é um serviço interno seu. Essa é a diferença material frente a um conversor de PDF em nuvem, onde o documento com CPF e condições financeiras precisa trafegar para um terceiro — o que exige base legal e contrato de operador sob a LGPD.

Qual é o limite de tamanho do template?

O corpo da requisição é limitado a 1 MB pelo middleware comum, o que cobre a esmagadora maioria dos contratos em HTML. Se você embutir imagens em base64 ou CSS extenso, esse teto chega rápido e a resposta é 413. Referencie imagens por URL em vez de embuti-las, ou ajuste o limite na infraestrutura.

Dá para o jurídico editar o template sem passar por engenharia?

Só se o jurídico escrever HTML, o que raramente é o caso. Este building block é uma API — não há editor. Quem precisa disso hoje monta uma interface própria em cima das rotas de PATCH e preview, ou avalia uma ferramenta de template em DOCX como o Carbone (§5). É uma limitação assumida.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md