E-Signature
BetaColeta de assinatura eletrônica em documento, com trilha de eventos por signatário
Você manda o contrato para assinar por API e acompanha, signatário a signatário, quem viu, quem assinou e quem recusou — com as credenciais do provedor cifradas e o documento isolado por cliente.
- Financeiras e fintechs que precisam de contrato assinado no fim de cada esteira de crédito
- Plataformas B2B que coletam aceite de vários signatários em ordem definida
- Operações que já têm um provedor de assinatura e não querem o código preso a ele
- Integração direta e específica com a API de um provedor de assinatura dentro do seu código
- Planilha de acompanhamento de quem já assinou e quem falta
- Envio manual de contrato por e-mail com pedido de devolução assinada
- Um provedor de assinatura — o BB orquestra, quem assina é o provedor configurado
- Uma autoridade certificadora nem um emissor de certificado ICP-Brasil
- Um gerador de documento (isso é o document-template)
17 endpoints em 4 recursos.
Resumo executivo
O E-Signature é a camada que leva um documento até as pessoas que precisam assiná-lo e acompanha o que acontece com ele. Você envia o arquivo e a lista de signatários; o building block cria a solicitação no provedor de assinatura configurado, guarda o vínculo e passa a registrar quem visualizou, quem assinou e quem recusou.
Na prática, ele resolve o fim da esteira de crédito. Sem ele, o contrato aprovado vira um e-mail com anexo, um pedido de devolução assinada e uma planilha de acompanhamento. Com ele, POST /requests devolve um identificador e uma URL de assinatura por signatário, e GET /requests/:id responde a qualquer momento em que pé está cada um.
Está em beta desde janeiro de 2026 e é o único dos três building blocks de documentos publicado com URL própria em AMBIENTES.md. Roda no monolito e em standalone na porta 3014, e está declarado nas pilhas de staging e de produção. Um único provedor está implementado — o OpenSign, auto-hospedado. Os demais aparecem no schema e recusam a operação em tempo de execução. Leia a §15 e a §14 antes de prometer prazo ou validade jurídica a cliente.
| Atributo | Valor |
|---|---|
| Identificador | e-signature |
| Categoria | Documentos |
| Escopo | Tenant (exige organizationId no token em todas as rotas, exceto a de webhook) |
| Porta (standalone) | 3014 |
| Path alias | @e-signature |
| Prefixo HTTP | /e-signature |
| Status | Beta desde 2026-01 |
| Depende de | PostgreSQL (schema esignature), Redis, S3, provedor de assinatura |
| Permissões | ESIGNATURE_ADMIN, ESIGNATURE_CREATE, ESIGNATURE_READ, ESIGNATURE_DELETE |
O problema
negócioO cenário. Uma financeira aprova uma proposta de crédito, gera a cédula e precisa que o devedor assine. Às vezes há um avalista, e ele precisa assinar depois. A operação só é liquidada quando todas as assinaturas estão coletadas, e alguém precisa saber, a qualquer momento, quantas faltam.
O que trava hoje.
- A integração fica presa a um provedor. O código chama a API do fornecedor escolhido, com o vocabulário dele: envelope, recipient, tab, template. Trocar de fornecedor — porque o preço subiu, porque o contrato acabou, porque o cliente exige outro — significa reescrever a integração inteira e migrar o histórico.
- O estado do envelope não conta a história. Saber que uma solicitação está "enviada" não responde se o devedor já viu, se o avalista recusou, ou se falta um dos dois. Quem precisa dessa resposta acaba montando uma planilha ao lado.
- A credencial do provedor mora no lugar errado. Chave de API em variável de ambiente compartilhada significa que todos os clientes da plataforma usam a mesma conta do provedor, e que rotacionar afeta todo mundo ao mesmo tempo.
- O documento circula por e-mail. Contrato com CPF, valor e condições de crédito indo e voltando como anexo é o pior caminho possível: não tem trilha, não tem controle de acesso e não tem prova de quem abriu.
- As duas testemunhas do papel viraram atrito no digital. Para um documento particular ser título executivo extrajudicial, o art. 784, III do CPC exige assinatura do devedor e de duas testemunhas. Coletar duas pessoas sem relação com o negócio, em fluxo digital, é o tipo de exigência que trava contrato pronto. A lei já resolveu isso — mas sob uma condição técnica que nem todo fornecedor cumpre (§14).
- A validade jurídica é presumida, não verificada. Muita gente assume que "assinatura eletrônica vale" sem saber qual nível de assinatura o fornecedor entrega, qual é o ônus da prova em cada nível, e em que hipóteses a lei exige mais do que isso.
O custo de não resolver. O custo operacional é o tempo entre aprovar o crédito e liquidar a operação — cada dia de contrato não assinado é capital parado e é risco de o cliente desistir. O custo estrutural aparece na renegociação com o fornecedor de assinatura: quando a integração é específica, a única alternativa a aceitar o reajuste é um projeto de meses. E o custo caro, que só aparece uma vez, é descobrir numa disputa que o nível de assinatura usado não sustenta o que você precisava provar.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| O código chama a API de um provedor específico | O provedor é uma configuração por organização, cifrada no banco |
| Uma conta do provedor compartilhada por todos os clientes | Cada organização configura a própria credencial, isolada e rotacionável |
| "Está enviado" é tudo que você sabe | Estado por signatário: pendente, enviado, visualizado, assinado, recusado |
| Contrato circula como anexo de e-mail | O arquivo vem do file-storage por identificador e não trafega a cada chamada |
| Trocar de fornecedor é reescrever a integração | A troca é uma configuração nova e um set-default |
O provedor é configuração, não dependência. Toda operação passa por uma interface canônica com oito métodos. A implementação concreta é escolhida por providerType na configuração da organização. Hoje há uma implementação — e é justamente por existir a interface que a segunda custa uma classe, não um projeto.
A credencial é cifrada por organização. O que você envia em credentials é serializado e cifrado com AES-256-GCM antes de tocar o banco, usando ESIGNATURE_CREDENTIAL_MASTER_KEY. Nenhuma rota devolve o campo cifrado nem o conteúdo dele.
O signatário é cidadão de primeira classe. Signer é uma tabela, com estado próprio e carimbos de sentAt, viewedAt, signedAt e declinedAt. "Quem falta assinar" é um filtro, não uma dedução a partir do estado do envelope.
O documento fica na plataforma. fileId aponta para um arquivo já guardado no file-storage. O building block resolve o download internamente e envia ao provedor. O contrato não precisa trafegar em base64 na sua chamada — embora essa opção também exista.
Casos de uso reais
negócioCaso 1 — Uma financeira coleta assinatura de devedor e avalista em ordem Cenário ilustrativo
Financeira de crédito pessoal com garantia de terceiro. Cada operação exige a assinatura do devedor e, depois dela, a do avalista.
O fluxo anterior mandava o contrato para os dois ao mesmo tempo por e-mail. Quando o avalista assinava antes do devedor, o documento voltava fora de ordem e a operação era rejeitada na conferência. Descobrir onde cada contrato estava exigia abrir a caixa de entrada e procurar.
POST /requests com dois signatários e o campo order — 1 para o devedor, 2 para o avalista. O provedor respeita a ordem de envio. A esteira acompanha por GET /requests/:id, que devolve o envelope e a lista de signatários com o estado de cada um.
sequenceDiagram
autonumber
participant App as Esteira de crédito
participant ES as E-Signature
participant Prov as Provedor OpenSign
participant Dev as Devedor — ordem 1
participant Ava as Avalista — ordem 2
App->>ES: POST /requests com dois signatários e order 1 e 2
ES->>Prov: cria o documento em modo sequencial
Prov-->>ES: externalId e signingUrl por signatário
ES-->>App: 201 com o envelope e os signatários
Prov->>Dev: convite com o link individual de assinatura
Dev-->>Prov: assina
Prov->>ES: webhook signed
Note over ES: falta o avalista, envelope vai a PARTIALLY_SIGNED
Prov->>Ava: só agora convida o avalista
Ava-->>Prov: assina
Prov->>ES: webhook signed
Note over ES: todos assinaram, envelope vai a COMPLETED
App->>ES: GET /requests filtrando por businessId
ES-->>App: estado atual de cada signatárioA ordem deixa de depender de disciplina e passa a ser propriedade da solicitação. E a pergunta "onde está o contrato da proposta 481" tem uma resposta em uma chamada, filtrando por businessId.
Caso 2 — Uma plataforma troca de provedor de assinatura sem parar Cenário ilustrativo
Plataforma B2B que atende dezenas de empresas clientes, cada uma com preferência e contrato próprios com fornecedores de assinatura.
A integração original chamava a API de um fornecedor específico, com o vocabulário dele espalhado por vários serviços. Quando um cliente grande exigiu usar o fornecedor com quem já tinha contrato, a resposta foi um projeto de três meses — e a alternativa, perder o cliente.
Cada organização cria a própria configuração em POST /provider-configs com o providerType e as credenciais dela. O resto do código chama sempre a mesma rota; qual provedor atende é decidido pela configuração padrão do tenant, ou pelo configId explícito na chamada.
flowchart LR
App["Sua aplicação<br/>sempre a mesma chamada"] --> ES["E-Signature<br/>interface canônica de 8 métodos"]
ES --> Res{"configId informado?"}
Res -->|sim| Cfg["a configuração indicada"]
Res -->|não| Pad["a configuração padrão do tenant<br/>isDefault e isActive"]
Cfg --> Fab["createESignatureProvider<br/>lê providerType e decifra credenciais"]
Pad --> Fab
Fab --> O["OPENSIGN — implementado"]
Fab --> D["DOCUSIGN — recusa em execução"]
Fab --> A["ADOBESIGN — recusa em execução"]
Fab --> Z["ZAPSIGN — recusa em execução"]A escolha de fornecedor vira dado, não código. Vale a ressalva honesta: hoje só o OpenSign está implementado, então esse caso descreve a arquitetura pronta e não uma capacidade disponível — ver §15.
Caso 3 — Uma operação para de perder contrato em caixa de e-mail Cenário ilustrativo
Operação de originação com cerca de 400 contratos por mês e três pessoas acompanhando o retorno das assinaturas.
O contrato saía por e-mail, e o acompanhamento era manual. Contratos ficavam parados por semanas porque ninguém percebia que o signatário nunca tinha aberto. Não havia diferença visível entre "não viu" e "viu e não quis assinar" — e essas duas situações pedem ações opostas.
Cada solicitação registra, por signatário, o instante da visualização e o da assinatura. Quem nunca visualizou recebe POST /requests/:id/resend com o e-mail do signatário. Quem recusou aparece com status: DECLINED e o motivo, quando o provedor o fornece.
flowchart LR
Fila["Fila de acompanhamento<br/>GET /requests"] --> Q{"estado do signatário"}
Q -->|"PENDING ou SENT — viewedAt vazio"| NV["Nunca abriu o documento"]
Q -->|"VIEWED — signedAt vazio"| VN["Abriu e não assinou"]
Q -->|DECLINED| RE["Recusou, com declineReason quando o provedor informa"]
Q -->|SIGNED| OK["Assinou"]
NV --> A1["Ação: reenviar<br/>POST /requests/:id/resend"]
VN --> A2["Ação: ligar para a pessoa<br/>reenviar não resolve"]
RE --> A3["Ação: tratar a objeção ou encerrar a operação"]
OK --> A4["Ação: nenhuma — aguardar os demais signatários"]A fila de acompanhamento passa a ser ordenável por estado real. Ressalva importante: hoje a atualização de estado depende do webhook do provedor, e a rota de webhook não está funcional (§15) — na prática, o acompanhamento fino ainda exige consulta ao painel do provedor.
Caso 4 — A lei dispensou as duas testemunhas, sob uma condição técnica Referência de mercado
O art. 784, III do Código de Processo Civil exige, para que um documento particular seja título executivo extrajudicial, a assinatura do devedor e de duas testemunhas. Coletar duas testemunhas em contrato digital sempre foi o atrito mais absurdo do processo — pessoas que não têm relação com o negócio precisam assinar para o título valer.
A Lei nº 14.620, de 13 de julho de 2023, no art. 34, acrescentou o § 4º ao art. 784 do CPC: "Nos títulos executivos constituídos ou atestados por meio eletrônico, é admitida qualquer modalidade de assinatura eletrônica prevista em lei, dispensada a assinatura de testemunhas quando sua integridade for conferida por provedor de assinatura." Duas consequências: qualquer modalidade serve — inclusive a simples — e as testemunhas caem, mas apenas sob a condição de a integridade ser conferida pelo provedor de assinatura. A condição é técnica, verificável, e é exatamente o que separa um provedor sério de um gerador de PDF com uma imagem colada.
flowchart LR
Doc["Documento particular em meio eletrônico"] --> Q1{"É CCB?"}
Q1 -->|sim| CCB["Art. 28 da Lei 10.931/2004<br/>título executivo por lei própria<br/>enquadra no art. 784, XII do CPC"]
Q1 -->|não| Q2{"A integridade é conferida<br/>pelo provedor de assinatura?"}
CCB --> Ass["Basta a assinatura do emitente<br/>art. 29, VI e § 5º<br/>eletrônica, com identificação inequívoca"]
Q2 -->|sim| S4["Art. 784, § 4º do CPC<br/>qualquer modalidade de assinatura serve<br/>as duas testemunhas são dispensadas"]
Q2 -->|não| S3["Art. 784, III do CPC<br/>continua exigindo duas testemunhas"]
S4 --> Ev{"Quem guarda a evidência<br/>de integridade hoje?"}
Ev --> P["OpenSign: hash SHA-256, trilha com IP<br/>e certificado de conclusão"]
Ev --> B["Building block: nenhuma das três — §15"]O provedor implementado, o OpenSign, produz as evidências que sustentam essa condição: hash SHA-256 do documento final, trilha de auditoria com endereço IP por evento e um certificado de conclusão. Ressalva que precisa vir junto e sem eufemismo: o building block ainda não captura nenhuma dessas três evidências — elas ficam no provedor e não são espelhadas no nosso banco (§15). É a lacuna mais relevante deste building block hoje, e é onde está o valor jurídico.
O caminho legal está aberto e é textualmente ancorado. O que falta é do nosso lado, e está identificado.
Mercado e diferenciais
negócioPanorama: três camadas, e nós não estamos em nenhuma delas
Panorama. O mercado de assinatura eletrônica tem três camadas. No topo, plataformas internacionais — DocuSign, Adobe Acrobat Sign, Dropbox Sign — que cobram por usuário emissor com franquia de envelopes e vendem alcance, integrações e certificado de conclusão. No meio, os brasileiros — Clicksign, D4Sign, ZapSign, Autentique — que cobram por volume de documentos, entendem o marco legal local e, em parte deles, suportam certificado ICP-Brasil. Embaixo, soluções abertas auto-hospedáveis, como o OpenSign.
Todos são provedores. O E-Signature da Catalisa não é: ele é a camada que fica entre a sua aplicação e o provedor, para que a escolha do provedor não seja uma decisão de arquitetura irreversível. A comparação abaixo é honesta sobre essa diferença de natureza.
flowchart TD
App["Sua aplicação"] --> Cat["Catalisa E-Signature<br/>camada de orquestração — não assina nada"]
Cat --> Int["Camada 1 — internacionais<br/>DocuSign · Adobe Acrobat Sign · Dropbox Sign<br/>cobram por usuário emissor, com franquia de envelopes"]
Cat --> Br["Camada 2 — brasileiros<br/>Clicksign · D4Sign · ZapSign · Autentique<br/>cobram por volume de documentos, marco legal local"]
Cat --> Open["Camada 3 — abertos e auto-hospedáveis<br/>OpenSign<br/>sem custo por documento, você opera a instância"]
Int -.->|não implementado| X1["§15"]
Br -.->|não implementado| X1
Open ==>|único implementado hoje| X2["§15"]O comparativo, critério a critério
| Critério | Catalisa E-Signature | DocuSign | Clicksign | D4Sign | ZapSign | Autentique | OpenSign |
|---|---|---|---|---|---|---|---|
| Natureza | Camada de orquestração | Provedor | Provedor | Provedor | Provedor | Provedor | Provedor auto-hospedável |
| Assina documentos | Não — delega ao provedor | Sim | Sim | Sim | Sim | Sim | Sim |
| Entrada publicada (2026-08-16) | Em definição | R$ 45/mês | R$ 39/mês | R$ 39,90/mês | Grátis até 3 docs; R$ 39,90/mês | Grátis até 10 docs; R$ 99/mês | Sem custo por documento |
| Preço de API público | — | R$ 2.560 a R$ 24.520/ano | Incluso desde o plano de entrada | Não publicado | A partir do plano intermediário | Tarifa por evento publicada em reais | — |
| Certificado ICP-Brasil | Depende do provedor | Sim, mas só nos planos sob cotação | Sim | Sim | É AC credenciada pelo ITI | Sim — A1, A3 e nuvem | Não |
| Nível da Lei 14.063 que declara produzir | O do provedor configurado | Não declara por plano | Alega cobrir os três | Não declara | Não declara | Declara avançada (art. 4º, II) | Não declara — o código indica simples |
| Troca de provedor sem reescrever | Sim, é o ponto | Não se aplica | Não se aplica | Não se aplica | Não se aplica | Não se aplica | Não se aplica |
| Credencial isolada por cliente | Sim, cifrada por organização | Você implementa | Você implementa | Você implementa | Você implementa | Você implementa | Você implementa |
| Estado por signatário | Sim, tabela própria | Sim | Sim | Sim | Sim | Sim | Sim |
| Documento fica na sua infraestrutura | Sim, se o provedor for auto-hospedado | Não | Não | Não | Não | Não | Sim |
| Assinatura de webhook verificável | Depende do provedor (§15) | Sim | Sim, HMAC | Não documentado | Sim | Sim | Não |
Preços consultados nas páginas oficiais em 2026-08-16: DocuSign, Clicksign, D4Sign, ZapSign e Autentique. Variam por faixa de volume e por negociação — confira na data da sua análise. O Dropbox Sign publica preço apenas em dólar e não localizamos nenhuma menção a ICP-Brasil nas páginas dele. A Adobe reconhece a raiz do ITI na sua lista de confiança, mas não publica preço de API nem documenta fluxo específico de ICP-Brasil. Onde escrevemos "não declara", significa que o fornecedor explica os níveis da lei mas não afirma qual deles produz.
Um detalhe que vale para quem compra
Um detalhe que vale para quem compra. Ao avaliar material jurídico de fornecedor, confira as citações. Verificando contra o texto do Planalto em 2026-08-16, encontramos duas imprecisões públicas: a Autentique cita "art. 4º, I" ao descrever a assinatura qualificada — no texto legal, o inciso I é a simples e a qualificada é o III —, e a D4Sign escreve "Lei 14.603/2020" onde a norma é a 14.063/2020. Não são erros graves, mas são o tipo de coisa que o jurídico do seu cliente vai notar, e é por isso que nesta documentação cada citação legal traz artigo e link.
Nossos diferenciais
Nossos diferenciais
- A escolha do provedor deixa de ser irreversível. A interface canônica tem oito métodos e todos os serviços falam com ela, nunca com o provedor. Isso é difícil de copiar não pela mecânica, mas porque exige ter resistido, desde o primeiro dia, à tentação de deixar o vocabulário do fornecedor vazar para dentro do domínio.
- A credencial é do tenant, cifrada, e nunca volta. Cada organização configura a própria conta no provedor, cifrada com AES-256-GCM. A função que monta a resposta simplesmente não inclui o campo de credenciais — não há caminho de serialização que vaze.
- Envelope e signatário têm estados independentes.
SignatureRequesttem oito estados;Signertem cinco. Isso permite o estado intermediárioPARTIALLY_SIGNED, que é onde a maioria dos contratos de crédito com avalista realmente vive. - O documento pode nunca sair da plataforma. Com
fileId, o arquivo vem do file-storage; com um provedor auto-hospedado, ele não sai da sua infraestrutura em momento algum. Em provedor de nuvem, isso é impossível por construção.
Quando escolher o concorrente
flowchart TD
Q1{"O ato exige assinatura qualificada<br/>com certificado ICP-Brasil?"}
Q1 -->|sim| C1["Contrate direto: Clicksign, D4Sign,<br/>ZapSign ou Autentique.<br/>A ZapSign é AC credenciada pelo ITI"]
Q1 -->|não| Q2{"Precisa de alcance internacional e<br/>certificado de conclusão em outra jurisdição?"}
Q2 -->|sim| C2["DocuSign"]
Q2 -->|não| Q3{"Quer começar amanhã,<br/>sem construir nada?"}
Q3 -->|sim| C3["Faixa gratuita da ZapSign ou do Autentique.<br/>Este building block sozinho não assina nada"]
Q3 -->|não| Q4{"Quantos contratos por mês?"}
Q4 -->|"meia dúzia"| C4["A camada de abstração é custo sem benefício"]
Q4 -->|"volume, com muitos clientes<br/>e um provedor por cliente"| C5["Catalisa E-Signature"]Quando escolher o concorrente. Se você precisa de assinatura qualificada com certificado ICP-Brasil — porque o ato exige, ou porque o seu jurídico exige —, contrate Clicksign, D4Sign, ZapSign ou Autentique diretamente: os quatro suportam certificado ICP-Brasil e o OpenSign não. A ZapSign, inclusive, é autoridade certificadora credenciada pelo ITI. Se o requisito é alcance internacional e certificado de conclusão que sobreviva a uma disputa em outra jurisdição, o DocuSign é a escolha estabelecida. Se você quer começar amanhã, com o menor esforço e sem construir nada, a faixa gratuita da ZapSign ou do Autentique resolve hoje, e este building block sozinho não assina nada. E se a sua operação envia meia dúzia de contratos por mês, a camada de abstração aqui é custo sem benefício.
O E-Signature ganha quando o problema é muitos clientes, cada um com o próprio provedor e a própria credencial, e uma integração que não pode ser refeita a cada renegociação de contrato — não quando o problema é assinar um documento.
Sobre a licença do OpenSign, e isto precisa ser dito
Sobre a licença do OpenSign, e isto precisa ser dito. O projeto se declara sob AGPL-3, mas o arquivo de licença tem uma exceção para o diretório customRoute, cuja licença remete a um arquivo que não existe naquele diretório — e é por isso que o GitHub classifica o repositório como NOASSERTION em vez de AGPL-3.0 (verificado em 2026-08-16). Some-se a isso que a própria AGPL-3 exige disponibilizar o código-fonte a quem interage com o software pela rede, o que é material para uma plataforma multi-tenant. Não use o OpenSign em produto comercial sem análise jurídica da licença. Registramos isso aqui porque é o tipo de risco que aparece tarde e caro.
Modelo de cobrança e ROI
negócioQuem cobra o quê nesta cadeia
flowchart LR
Cli["Cliente"] -->|"contrato Catalisa — precificação em definição"| Cat["Catalisa E-Signature<br/>orquestra"]
Cli -->|"contrato direto, sem intermediação da Catalisa"| Prov["Provedor de assinatura<br/>assina"]
Cat -.->|"usa a credencial do cliente"| Prov
Prov -->|"cobra por documento, por usuário<br/>ou só infraestrutura, se auto-hospedado"| CliPrecificação em definição
Precificação em definição. O E-Signature não tem preço fechado. Ele é a camada de orquestração de uma cadeia — geração, assinatura e leitura de documento — e a intenção é que acompanhe a contratação do conjunto. Não há valor a divulgar e este documento não estima nenhum.
O custo que não é nosso
O custo que não é nosso. O custo da assinatura em si é do provedor que você contratar, e vai direto para você. Essa separação é deliberada e é boa para o cliente: a Catalisa não intermedia a relação comercial com o provedor, não estende linha de crédito e não embute margem no preço por documento. Se você escolher um provedor auto-hospedado, esse custo é apenas infraestrutura.
O que dispara custo
O que dispara custo.
| Driver | Por quê |
|---|---|
| Solicitações de assinatura criadas | Cada uma envolve upload do documento ao provedor, criação de contatos e gravação de envelope, signatários e documentos |
| Número de signatários por solicitação | O provedor cria um contato e um conjunto de campos por signatário, e a maioria cobra por isso |
| Tamanho do documento | O arquivo é carregado em memória em base64 antes de subir ao provedor |
| Consultas de estado | Enquanto o webhook não estiver funcional (§15), acompanhar estado exige consulta ativa |
Comparação de custo em cenário nomeado
Comparação de custo — cenário nomeado: financeira que coleta assinatura em 400 contratos por mês, com dois signatários cada, e opera com três pessoas no time que dispara e acompanha.
| Catalisa + OpenSign auto-hospedado | DocuSign | Clicksign | ZapSign | Autentique | |
|---|---|---|---|---|---|
| Base de cálculo | Precificação em definição; sem custo por documento | Por usuário emissor, mais franquia de envio por API | Por faixa de documentos, com excedente | Por faixa, com faixa gratuita e recursos avulsos | Documentos ilimitados no plano profissional |
| Ordem de grandeza mensal do provedor | Só a infraestrutura da instância | Centenas de reais por usuário, mais contrato de API a partir de R$ 2.560/ano | Dezenas a centenas de reais, conforme a faixa | Dezenas de reais, mais avulsos por recurso | R$ 99/mês, mais eventos avulsos |
| Custo por documento excedente | Zero | Consumo de franquia | R$ 2,40 a R$ 6,90 | Conforme plano | Tarifa por evento publicada |
| ICP-Brasil disponível | Não | Sim, sob cotação | Sim | Sim | Sim |
| Você opera o provedor | Sim | Não | Não | Não | Não |
Preços dos fornecedores consultados em 2026-08-16 nas páginas citadas na §5. A coluna da Catalisa não é comparável linha a linha: o building block não substitui o provedor, ele o orquestra. Se você escolher um provedor comercial, o custo dele continua existindo.
ROI
ROI. A conta de guardanapo tem duas linhas.
| Linha da conta | O que se ganha | O que ressalvar |
|---|---|---|
| Tempo entre aprovar e liquidar | Dias viram horas; o ganho é proporcional ao ticket, não ao número de contratos | Depende de o acompanhamento funcionar — hoje o webhook não está funcional (§15) |
| Custo de troca de fornecedor | A troca vira configuração, e o poder de negociação deixa de ser todo do fornecedor | Valor arquitetural ainda não realizado: não existe uma segunda implementação de provedor (§15) |
A primeira é o tempo entre aprovar e liquidar. Numa operação de crédito, cada dia de contrato não assinado é capital parado e é probabilidade de desistência. Substituir e-mail com anexo por solicitação rastreável costuma cortar esse intervalo de dias para horas — e o ganho é proporcional ao ticket, não ao número de contratos.
A segunda é o custo de troca de fornecedor. Com integração específica, a alternativa a aceitar um reajuste é um projeto de meses; na prática, o fornecedor sabe disso e o poder de negociação é dele. Com a camada de abstração, a troca é uma configuração — desde que exista uma segunda implementação de provedor, que hoje não existe (§15). É um valor arquitetural real e ainda não realizado, e é assim que ele deve ser apresentado a cliente.
Arquitetura
As camadas e o caminho da requisição
flowchart TD
HTTP["HTTP — Bearer JWT emitido pelo IAM"]
subgraph App["Hono app · basePath /e-signature · applyCommonMiddleware"]
R1["/api/v1/esignature/provider-configs<br/>providerConfigRouter — 7 rotas"]
R2["/api/v1/esignature/requests<br/>signatureRequestRouter — 8 rotas"]
R3["/api/v1/esignature/webhooks<br/>webhookRouter — 1 rota, pública"]
R4["/health<br/>sonda pública"]
end
subgraph Svc["services/"]
SV1["ProviderConfigService<br/>CRUD da configuração e testar conexão"]
SV2["SignatureRequestService<br/>criar · cancelar · reenviar · baixar"]
SV3["WebhookHandlerService<br/>verificar → registrar → aplicar estado"]
end
subgraph Fab["providers/ · createESignatureProvider por providerType"]
P1["OPENSIGN — implementado<br/>OpenSignProvider sobre Parse REST"]
P2["DOCUSIGN — recusa com INTERNAL not yet implemented"]
P3["ADOBESIGN — idem"]
P4["ZAPSIGN — idem"]
end
Repo["repositories/ — 4, via Prisma<br/>PostgreSQL, schema esignature"]
Cripto["utils/crypto — AES-256-GCM<br/>ESIGNATURE_CREDENTIAL_MASTER_KEY<br/>formato iv:authTag:ciphertext"]
Bucket["S3 ou MinIO — file-storage"]
OS["Instância OpenSign<br/>Parse Server REST API"]
HTTP --> App
App -->|"Zod parse, ResultAsync e handleResult"| Svc
Svc --> Repo
Svc --> Cripto
Cripto -->|"decifra em memória"| Fab
Svc -->|"resolve fileId por URL assinada"| Bucket
Fab -->|HTTPS| OS
OS -->|"webhook do provedor"| R3Caminho de uma solicitação de assinatura
Caminho de uma solicitação de assinatura
flowchart TD
A["POST /requests<br/>title, fileId ou fileBase64, signers, widgets opcionais"] --> B{"configId informado?"}
B -->|sim| C["usa a configuração indicada"]
B -->|não| D["busca a padrão da organização<br/>isDefault e isActive"]
D --> E{"encontrou alguma?"}
E -->|não| F["404 ProviderConfig"]
E -->|sim| G
C --> G{"origem do arquivo"}
G -->|fileId| H["URL assinada no S3 → download → base64 em memória"]
G -->|fileBase64| I["usa direto"]
H --> J["decifra as credenciais com a chave mestra"]
I --> J
J --> K{"providerType implementado?"}
K -->|não| L["500 INTERNAL — not yet implemented"]
K -->|sim| M["provider.createSignatureRequest"]
M --> N["No OpenSign: upload do arquivo → cria ou reaproveita<br/>os contatos dos signatários → monta os campos<br/>de assinatura → cria o documento"]
N --> O["Grava na mesma transação:<br/>SignatureRequest + Signer + SignatureDocument"]
O --> P["Publica esignature.request.created"]
P --> Q["201 com o envelope e os signatários"]O fluxo de assinatura ponta a ponta: criar, enviar, assinar, selar
O diagrama acima para na criação. Este continua a história até o documento selado voltar para você — e mostra, em cada troca, quem depende de quem.
sequenceDiagram
autonumber
participant App as Sua aplicação
participant ES as E-Signature
participant S3 as file-storage — S3 ou MinIO
participant Prov as Provedor de assinatura
participant Sig as Signatário
rect rgb(240, 245, 255)
Note over App,Prov: 1. Criar
App->>ES: POST /requests com fileId e signers
ES->>S3: URL assinada e download do arquivo
S3-->>ES: PDF convertido em base64 em memória
ES->>ES: decifra as credenciais do tenant em AES-256-GCM
ES->>Prov: cria o documento, os contatos e os campos de assinatura
Prov-->>ES: externalId, viewUrl e signingUrl por signatário
ES-->>App: 201 com o envelope gravado em DRAFT ou SENT
end
rect rgb(245, 250, 240)
Note over Prov,Sig: 2. Enviar ao signatário
Prov->>Sig: e-mail com o link individual de assinatura
Sig->>Prov: abre o documento
Prov->>ES: webhook viewed
ES->>ES: envelope de SENT ou DRAFT para VIEWED, signatário para VIEWED
end
rect rgb(255, 248, 240)
Note over Sig,ES: 3. Assinar
Sig->>Prov: assina
Prov->>ES: webhook signed
alt todos os signatários assinaram
ES->>ES: envelope para COMPLETED e completedAt preenchido
else ainda falta alguém
ES->>ES: envelope para PARTIALLY_SIGNED
end
end
rect rgb(250, 240, 245)
Note over Prov,App: 4. Selar e devolver
Prov->>Prov: sela o PDF com o certificado do servidor e calcula o hash SHA-256
App->>ES: GET /requests/:id/download
ES->>ES: recusa com VALIDATION se o envelope não estiver COMPLETED
ES->>Prov: pede a URL do documento assinado
Prov-->>ES: downloadUrl temporária
ES-->>App: downloadUrl e expiresAt — sem proxy do binário
endAtenção. Os passos 2, 3 e o webhook do passo 4 dependem da rota de webhook,
que não está funcional hoje (§15). Na prática, o envelope permanece no estado
gravado na criação até que alguém o atualize, e o selo e o hash do passo 4 ficam
apenas do lado do provedor (§14).
Decisões não óbvias
Decisões não óbvias.
- A credencial é cifrada por organização, não configurada por ambiente. É o que torna o building block multi-tenant de verdade: cada cliente conecta a própria conta no provedor. O
ESIGNATURE_CREDENTIAL_MASTER_KEYé apenas a chave de cifragem, comum ao serviço. O trade-off é que ela vira ativo crítico — vazá-la expõe as credenciais de todos os tenants —, e por isso é validada como 64 caracteres hexadecimais e deve ficar em SOPS. - A verificação da assinatura do webhook é fail-closed, mas o provedor não a oferece. Desde a correção do incidente em que todos os
verifyWebhookSignatureretornavamtrueincondicionalmente, um resultado inválido interrompe o processamento. O OpenSign, porém, não tem esquema de assinatura de webhook, e o provider registra isso explicitamente. O comportamento passa a depender dePROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT: comoff(padrão), o webhook é aceito e um aviso é registrado; comon, ele é recusado. Não há terceira opção enquanto o provedor não oferecer assinatura — a mitigação correta é lista de IPs permitidos na borda. - Criar a configuração não testa a conexão. O
ProviderConfigService.createcifra e grava sem chamar o provedor. Uma credencial errada, ou umproviderTypenão implementado, é aceita e só falha na primeira solicitação real. ExistePOST /provider-configs/:id/testexatamente para isso — use-o depois de criar, sempre. - Cancelar, no OpenSign, é marcar como expirado. O Parse Server do OpenSign não tem um estado de cancelamento; o provider define
Status: 'expired'no documento remoto, enquanto o nosso banco gravaCANCELLED. Os dois lados ficam com nomes diferentes para o mesmo fato, de propósito: o nosso vocabulário é canônico e o do provedor não precisa ser. E o cancelamento tolera respostas404,409e422do provedor, tratando-as como já cancelado. - O reenvio não é um reenvio de verdade. O OpenSign não expõe endpoint de reenvio na API REST; o provider atualiza o campo
SentToOthersdo documento para disparar o fluxo de notificação. Funciona por efeito colateral, não por contrato — e é frágil a mudanças do lado do provedor. - O download devolve a URL do provedor, não o arquivo.
GET /requests/:id/downloadresponde comdownloadUrleexpiresAt; o serviço não faz proxy do binário. É a escolha certa para não carregar PDF em memória a cada consulta, e o custo é que a URL é do provedor e caduca conforme as regras dele — oexpiresAtpadrão de uma hora é um palpite nosso, não uma garantia dele. - O arquivo por
fileIdpassa inteiro pela memória. O serviço gera a URL assinada no S3, baixa e converte em base64 antes de subir ao provedor. Para contrato de poucas páginas é irrelevante; para documento grande, é o ponto de pressão de memória do serviço.
Monolito vs. standalone. O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/e-signature. O main.ts sobe o mesmo app com Bun.serve na porta 3014 quando DEPLOYMENT_MODE=standalone, que é o modo usado em staging e produção. Não há diferença de comportamento entre os modos. O que muda é que, em standalone, applyCommonMiddleware é a única fonte de limite de corpo, CORS, cabeçalhos de segurança e limite de taxa.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Solicitação de assinatura (envelope) | A unidade de trabalho: um documento, um conjunto de signatários e um estado. Corresponde ao modelo SignatureRequest. |
| Signatário | Uma pessoa que precisa assinar, com nome, e-mail, ordem e estado próprio. Corresponde ao modelo Signer. |
Ordem (order) | Inteiro a partir de 1. Quando algum signatário tem ordem maior que 1, a solicitação é criada no provedor em modo sequencial. |
Campo de assinatura (widget) | Onde, na página, o signatário assina ou preenche. Tipo (signature, text, date, checkbox), página e coordenadas. Sem widgets, o building block posiciona um campo de assinatura por signatário na primeira página. |
| Configuração de provedor | O provedor escolhido pela organização e suas credenciais cifradas. Uma delas é a padrão. |
externalId | O identificador do recurso no provedor. É por ele que o webhook reencontra a solicitação. Único em conjunto com a configuração. |
businessId | Identificador livre da operação de origem — proposta, contrato, pedido. É o campo de correlação com os outros building blocks. |
| Chave mestra de credenciais | ESIGNATURE_CREDENTIAL_MASTER_KEY. Chave AES-256-GCM de 32 bytes, informada como 64 caracteres hexadecimais, usada para cifrar as credenciais de todos os tenants. |
| Assinatura eletrônica simples | Na classificação da Lei nº 14.063/2020, o nível que permite identificar o signatário e anexar dados ao documento, sem exigir certificado. É o nível que o provedor implementado hoje entrega — ver §14. |
| OpenSign | Plataforma de assinatura de código aberto, auto-hospedável, construída sobre Parse Server. É o único provedor implementado. |
Modelo de dados — schema esignature no PostgreSQL. 5 modelos.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
ESignatureProviderConfig | esignature_provider_configs | Provedor e credenciais da organização | Único (organizationId, name), providerType, credentials (cifrado), isDefault, isActive, settings, deletedAt |
SignatureRequest | signature_requests | O envelope | Único (configId, externalId), title, status, businessId, templateId, viewUrl, expiresAt, completedAt, deletedAt |
Signer | signers | Um signatário do envelope | externalId, email, order, status, signingUrl, sentAt, viewedAt, signedAt, declinedAt, declineReason |
SignatureDocument | signature_documents | O arquivo vinculado ao envelope | externalId, fileId, originalUrl, signedUrl, signedUrlExpiresAt |
ESignatureWebhookEvent | esignature_webhook_events | Todo webhook recebido, cru | eventType, externalEventId, payload, processed, processedAt, error |
erDiagram
ORGANIZATION ||--o{ ESIGNATURE_PROVIDER_CONFIG : "configura"
ESIGNATURE_PROVIDER_CONFIG ||--o{ SIGNATURE_REQUEST : "atende"
ESIGNATURE_PROVIDER_CONFIG ||--o{ ESIGNATURE_WEBHOOK_EVENT : "recebe por"
SIGNATURE_REQUEST ||--o{ SIGNER : "tem, em cascata"
SIGNATURE_REQUEST ||--o{ SIGNATURE_DOCUMENT : "tem, em cascata"
SIGNATURE_REQUEST ||--o{ ESIGNATURE_WEBHOOK_EVENT : "é atualizada por"
ESIGNATURE_PROVIDER_CONFIG {
uuid id PK
uuid organization_id "isolamento do tenant"
string name "único junto com organization_id"
enum provider_type "OPENSIGN DOCUSIGN ADOBESIGN ZAPSIGN"
text credentials "cifrado em AES-256-GCM, nunca retornado"
boolean is_default
boolean is_active
jsonb settings
timestamp deleted_at "exclusão lógica"
}
SIGNATURE_REQUEST {
uuid id PK
uuid organization_id "isolamento do tenant"
uuid config_id FK
string external_id "id no provedor, único junto com config_id"
string title
text message
enum status "8 estados — ver máquina abaixo"
string business_id "correlação com a operação de origem"
string template_id "template do PROVEDOR, não do document-template"
string template_name
text view_url
timestamp expires_at
timestamp completed_at
timestamp deleted_at "exclusão lógica"
}
SIGNER {
uuid id PK
uuid request_id FK
string external_id "id do contato no provedor"
string name
string email "chave de correlação com os eventos do provedor"
int order "1 por padrão; maior que 1 ativa modo sequencial"
enum status "PENDING SENT VIEWED SIGNED DECLINED"
text signing_url "URL com poder — trate como segredo"
timestamp sent_at
timestamp viewed_at
timestamp signed_at
timestamp declined_at
text decline_reason
}
SIGNATURE_DOCUMENT {
uuid id PK
uuid request_id FK
string external_id
string name
uuid file_id "aponta para o file-storage"
text original_url
text signed_url
timestamp signed_url_expires_at
}
ESIGNATURE_WEBHOOK_EVENT {
uuid id PK
uuid organization_id
uuid config_id FK
uuid request_id FK "nulo quando o evento não casa com envelope"
string event_type "viewed signed completed declined"
string external_event_id
jsonb payload "corpo original, sempre gravado"
boolean processed
timestamp processed_at
text error "motivo da falha de processamento"
}Configurações, solicitações e eventos de webhook são isolados por organizationId. Signatários e documentos herdam o isolamento da solicitação e são removidos em cascata com ela.
Enumerações
| Enum | Valores |
|---|---|
ESignatureProviderType | OPENSIGN (implementado) · DOCUSIGN · ADOBESIGN · ZAPSIGN (declarados, recusam em execução — §15) |
SignatureRequestStatus | DRAFT · SENT · VIEWED · PARTIALLY_SIGNED · COMPLETED · DECLINED · EXPIRED · CANCELLED |
SignerStatus | PENDING · SENT · VIEWED · SIGNED · DECLINED |
São duas máquinas de estado diferentes — a do envelope e a do signatário — e confundi-las é a origem mais comum de bug de integração neste building block. O envelope responde "a operação acabou?"; o signatário responde "quem falta?".
Máquina de estados do envelope
Máquina de estados do envelope
stateDiagram-v2
direction TB
[*] --> DRAFT: POST /requests
DRAFT: DRAFT — inicial, gravado quando o provedor ainda não reportou envio
SENT: SENT — o provedor reportou o envio na criação
VIEWED: VIEWED — alguém abriu o documento
PARTIALLY_SIGNED: PARTIALLY_SIGNED — um assinou e ainda falta alguém
COMPLETED: COMPLETED — todos assinaram, completedAt preenchido
DECLINED: DECLINED — alguém recusou
CANCELLED: CANCELLED — cancelado pela API
EXPIRED: EXPIRED — declarado no enum e nunca gravado hoje, §15
DRAFT --> SENT: provedor reporta envio
DRAFT --> VIEWED: webhook viewed
SENT --> VIEWED: webhook viewed
VIEWED --> PARTIALLY_SIGNED: webhook signed, ainda falta signatário
VIEWED --> COMPLETED: webhook signed, todos assinaram
PARTIALLY_SIGNED --> PARTIALLY_SIGNED: webhook signed, ainda falta signatário
PARTIALLY_SIGNED --> COMPLETED: webhook signed, todos assinaram
SENT --> COMPLETED: webhook completed
VIEWED --> COMPLETED: webhook completed
DRAFT --> DECLINED: webhook declined
SENT --> DECLINED: webhook declined
VIEWED --> DECLINED: webhook declined
PARTIALLY_SIGNED --> DECLINED: webhook declined
DRAFT --> CANCELLED: POST /requests/:id/cancel
SENT --> CANCELLED: POST /requests/:id/cancel
VIEWED --> CANCELLED: POST /requests/:id/cancel
PARTIALLY_SIGNED --> CANCELLED: POST /requests/:id/cancel
DECLINED --> CANCELLED: POST /requests/:id/cancel
COMPLETED --> [*]
CANCELLED --> [*]
EXPIRED --> [*]
note right of COMPLETED
COMPLETED é condição para
GET /requests/:id/download
end note
note right of CANCELLED
Cancelar é bloqueado quando o
status já é COMPLETED ou CANCELLED:
o serviço responde VALIDATION
end noteTrês leituras que o diagrama não diz sozinho:
| Observação | Consequência prática |
|---|---|
DRAFT e SENT dependem do que o provedor reporta na criação | O estado inicial não é escolha sua |
VIEWED, PARTIALLY_SIGNED, COMPLETED e DECLINED dependem do webhook, que hoje não está funcional (§15) | Na prática, o estado gravado é o da criação até que alguém o atualize |
EXPIRED está no enum, é mapeado a partir do provedor, e nenhum caminho do código o persiste | Envelope vencido continua no último estado conhecido (§15) |
Máquina de estados do signatário
Máquina de estados do signatário
stateDiagram-v2
direction LR
[*] --> PENDING: criação do envelope
PENDING: PENDING — ainda sem URL de assinatura
SENT: SENT — sentAt preenchido, o provedor emitiu a URL de assinatura
VIEWED: VIEWED — viewedAt preenchido
SIGNED: SIGNED — signedAt preenchido
DECLINED: DECLINED — declinedAt preenchido
PENDING --> SENT: o provedor emite a URL de assinatura
PENDING --> VIEWED: webhook viewed com o e-mail do signatário
SENT --> VIEWED: webhook viewed com o e-mail do signatário
PENDING --> SIGNED: webhook signed com o e-mail do signatário
SENT --> SIGNED: webhook signed com o e-mail do signatário
VIEWED --> SIGNED: webhook signed com o e-mail do signatário
PENDING --> DECLINED: webhook declined com o e-mail do signatário
SENT --> DECLINED: webhook declined com o e-mail do signatário
VIEWED --> DECLINED: webhook declined com o e-mail do signatário
SIGNED --> [*]
DECLINED --> [*]Como as duas máquinas se conversam:
- O envelope vira
COMPLETEDquando todos os signatários estãoSIGNED. - Enquanto faltar um, o envelope fica
PARTIALLY_SIGNED. - A correspondência entre o webhook e o signatário é feita pelo e-mail — por isso dois signatários com o mesmo e-mail quebram o acompanhamento (§16).
- O
declineReasonexiste como coluna, mas o tratamento do webhookdeclinedgrava apenasstatusedeclinedAt; o motivo depende do provedor informá-lo por outro caminho.
Referência da API
Prefixo HTTP: /e-signature. Em monolito, a base é http://localhost:3000. Em standalone, a porta é 3014; em staging, https://esignature.bb.stg.catalisa.app.
Atenção ao prefixo. O caminho real repete o nome do módulo, e com grafias diferentes: o basePath do app é /e-signature, com hífen, e os routers são montados em /api/v1/esignature, sem hífen. A rota completa é /e-signature/api/v1/esignature/.... Não é erro de digitação — copie do quadro abaixo em vez de deduzir.
15 das 16 rotas exigem authMiddleware (Bearer JWT do IAM), requirePermission e o middleware local requireOrganization, que devolve 403 quando o token não carrega organizationId. A exceção é a rota de webhook, que é pública por natureza — o provedor não tem token da plataforma.
Configurações de provedor — /e-signature/api/v1/esignature/provider-configs
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /e-signature/api/v1/esignature/provider-configs | Cria configuração e cifra as credenciais. Responde 201 | ESIGNATURE_ADMIN |
GET | /e-signature/api/v1/esignature/provider-configs | Lista as configurações da organização | ESIGNATURE_READ |
GET | /e-signature/api/v1/esignature/provider-configs/:id | Busca uma configuração (sem as credenciais) | ESIGNATURE_READ |
PATCH | /e-signature/api/v1/esignature/provider-configs/:id | Atualiza nome, credenciais, ajustes ou estado | ESIGNATURE_ADMIN |
DELETE | /e-signature/api/v1/esignature/provider-configs/:id | Exclusão lógica. Responde 204 | ESIGNATURE_ADMIN |
POST | /e-signature/api/v1/esignature/provider-configs/:id/test | Testa a conexão com o provedor | ESIGNATURE_ADMIN |
POST | /e-signature/api/v1/esignature/provider-configs/:id/set-default | Marca como padrão e desmarca as demais | ESIGNATURE_ADMIN |
Solicitações de assinatura — /e-signature/api/v1/esignature/requests
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /e-signature/api/v1/esignature/requests | Cria a solicitação a partir de um arquivo. Responde 201 | ESIGNATURE_CREATE |
POST | /e-signature/api/v1/esignature/requests/from-template | Cria a partir de um template do provedor. Responde 201 | ESIGNATURE_CREATE |
GET | /e-signature/api/v1/esignature/requests | Lista solicitações, paginado | ESIGNATURE_READ |
GET | /e-signature/api/v1/esignature/requests/:id | Busca a solicitação com os signatários | ESIGNATURE_READ |
POST | /e-signature/api/v1/esignature/requests/:id/cancel | Cancela a solicitação | ESIGNATURE_CREATE |
POST | /e-signature/api/v1/esignature/requests/:id/resend | Reenvia a todos ou a um signatário | ESIGNATURE_CREATE |
GET | /e-signature/api/v1/esignature/requests/:id/download | URL do documento assinado. Exige COMPLETED | ESIGNATURE_READ |
DELETE | /e-signature/api/v1/esignature/requests/:id | Exclusão lógica. Responde 204 | ESIGNATURE_DELETE |
Filtros aceitos em GET /requests: filter[status] e filter[businessId]. Paginação por page[number] e page[size].
Webhook do provedor
| Método | Rota | Descrição | Autenticação |
|---|---|---|---|
POST | /e-signature/api/v1/esignature/webhooks/:configId | Recebe evento do provedor | Pública — sem JWT. Ver §14 e §15 |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /e-signature/health | Sonda de disponibilidade. Pública, não contabilizada nas 16 rotas |
POST /e-signature/api/v1/esignature/provider-configs
Cria a configuração e cifra as credenciais antes de gravar. O corpo aceita tanto o envelope JSON:API (data.attributes) quanto o objeto direto.
Request
{
"name": "OpenSign Produção",
"providerType": "OPENSIGN",
"credentials": {
"baseUrl": "https://opensign.suaempresa.com.br",
"appId": "opensign",
"apiPrefix": "/api/app",
"masterKey": "<segredo do Parse Server>"
},
"isDefault": true,
"isActive": true
}{
"name": "OpenSign Produção",
"providerType": "OPENSIGN",
"credentials": {
"baseUrl": "https://opensign.suaempresa.com.br",
"appId": "opensign",
"apiPrefix": "/api/app",
"masterKey": "<segredo do Parse Server>"
},
"isDefault": true,
"isActive": true
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Nome da configuração. Único por organização |
providerType | OPENSIGN | DOCUSIGN | ADOBESIGN | ZAPSIGN | Sim | Só OPENSIGN funciona hoje (§15) |
credentials | objeto de textos | Sim | Cifrado antes de gravar. As chaves dependem do provedor |
isDefault | booleano | Não | Sem valor, a primeira configuração da organização vira a padrão |
isActive | booleano | Não | Padrão true |
settings | objeto | Não | Ajustes específicos do provedor |
Credenciais do OpenSign
| Chave | Obrigatória | Descrição |
|---|---|---|
baseUrl | Sim | URL da instância OpenSign. Sem ela, a criação do provedor falha |
masterKey | Uma das duas | Chave mestra do Parse Server. Acesso administrativo — evite em produção |
sessionToken | Uma das duas | Token de sessão de um usuário. Requer também extUserId |
extUserId | Com sessionToken | Identificador do registro contracts_Users do usuário |
appId | Não | Identificador da aplicação Parse. Padrão opensign |
apiPrefix | Não | Padrão /api/app, para instalação atrás de proxy. Use /app no acesso direto |
Resposta 201 — devolve a configuração sem o campo credentials. O segredo não volta em nenhuma rota, nunca.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod, ou providerType fora do enum |
403 | — | Sem ESIGNATURE_ADMIN, ou token sem organizationId |
409 | CONFLICT | Já existe configuração com esse nome na organização |
500 | INTERNAL | ESIGNATURE_CREDENTIAL_MASTER_KEY ausente ou malformada — a cifragem falha |
A criação não testa a conexão. Chame
POST /:id/testlogo em seguida; sem isso, uma credencial errada só aparece na primeira solicitação real.
POST /e-signature/api/v1/esignature/requests
Cria a solicitação e a envia ao provedor.
Request
{
"title": "CCB - Proposta 2026-000481",
"message": "Por favor, assine a cédula de crédito.",
"fileId": "8f2c1a90-0000-0000-0000-000000000000",
"businessId": "proposta-2026-000481",
"expiresInDays": 15,
"signers": [
{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 },
{ "name": "João Souza", "email": "joao@exemplo.com.br", "order": 2 }
],
"widgets": [
{ "type": "signature", "page": 3, "x": 100, "y": 620, "width": 150, "height": 50, "required": true }
]
}{
"title": "CCB - Proposta 2026-000481",
"message": "Por favor, assine a cédula de crédito.",
"fileId": "8f2c1a90-0000-0000-0000-000000000000",
"businessId": "proposta-2026-000481",
"expiresInDays": 15,
"signers": [
{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 },
{ "name": "João Souza", "email": "joao@exemplo.com.br", "order": 2 }
],
"widgets": [
{ "type": "signature", "page": 3, "x": 100, "y": 620, "width": 150, "height": 50, "required": true }
]
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
title | string (1–255) | Sim | Título do envelope |
message | string (máx. 2000) | Não | Mensagem aos signatários |
configId | UUID | Não | Configuração a usar. Omitido, usa a padrão da organização |
fileId | UUID | Um dos dois | Arquivo já guardado no file-storage |
fileBase64 | string | Um dos dois | Arquivo em base64. Exige fileName |
fileName | string (máx. 255) | Com fileBase64 | Nome do arquivo, usado para deduzir o tipo |
signers | lista, mínimo 1 | Sim | Cada item com name, email e order opcional |
widgets | lista | Não | Campos de assinatura. Sem eles, um campo por signatário na primeira página |
businessId | string (máx. 100) | Não | Identificador da operação de origem |
expiresInDays | inteiro 1–365 | Não | No OpenSign, vira TimeToCompleteDays; padrão do provedor é 15 |
Resposta 201
{
"data": {
"type": "signature-request",
"id": "3c8a1e77-0000-0000-0000-000000000000",
"links": { "self": "/api/v1/esignature/requests/3c8a1e77-0000-0000-0000-000000000000" },
"attributes": {
"title": "CCB - Proposta 2026-000481",
"status": "DRAFT",
"businessId": "proposta-2026-000481",
"viewUrl": "https://opensign.suaempresa.com.br/...",
"expiresAt": "2026-08-31T00:00:00.000Z",
"createdAt": "2026-08-16T14:03:11.000Z"
},
"relationships": {
"signers": {
"data": [
{ "type": "signer", "id": "a1...", "attributes": { "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1, "status": "SENT", "signingUrl": "https://..." } },
{ "type": "signer", "id": "b2...", "attributes": { "name": "João Souza", "email": "joao@exemplo.com.br", "order": 2, "status": "PENDING" } }
]
}
}
}
}{
"data": {
"type": "signature-request",
"id": "3c8a1e77-0000-0000-0000-000000000000",
"links": { "self": "/api/v1/esignature/requests/3c8a1e77-0000-0000-0000-000000000000" },
"attributes": {
"title": "CCB - Proposta 2026-000481",
"status": "DRAFT",
"businessId": "proposta-2026-000481",
"viewUrl": "https://opensign.suaempresa.com.br/...",
"expiresAt": "2026-08-31T00:00:00.000Z",
"createdAt": "2026-08-16T14:03:11.000Z"
},
"relationships": {
"signers": {
"data": [
{ "type": "signer", "id": "a1...", "attributes": { "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1, "status": "SENT", "signingUrl": "https://..." } },
{ "type": "signer", "id": "b2...", "attributes": { "name": "João Souza", "email": "joao@exemplo.com.br", "order": 2, "status": "PENDING" } }
]
}
}
}
}O signingUrl de cada signatário é a URL individual de assinatura. Trate-a como segredo: quem tem o link consegue assinar.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Nenhum ou os dois de fileId/fileBase64; fileBase64 sem fileName; lista de signatários vazia |
404 | NOT_FOUND | configId inexistente, ou a organização não tem configuração padrão ativa |
413 | — | Corpo acima de 1 MB — o teto prático de fileBase64 (§15) |
500 | INTERNAL | providerType declarado mas não implementado; falha ao baixar o arquivo do S3; erro do provedor |
GET /e-signature/api/v1/esignature/requests/:id
Devolve o envelope com os signatários aninhados. É a rota de acompanhamento.
Resposta 200 — mesmo formato da criação, com os estados e carimbos atualizados. Cada signatário traz status, sentAt, viewedAt, signedAt, declinedAt e declineReason.
Enquanto o webhook não estiver funcional (§15), estes campos refletem o que o provedor reportou no momento da criação. Não existe rota que force a releitura do estado no provedor — ver §15.
POST /e-signature/api/v1/esignature/requests/:id/cancel
Cancela a solicitação no provedor e grava CANCELLED.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Cannot cancel request with status COMPLETED ou CANCELLED |
404 | NOT_FOUND | Solicitação inexistente, excluída ou de outra organização |
No OpenSign, o cancelamento marca o documento remoto como expirado. Respostas 404, 409 e 422 do provedor são toleradas — a gravação local prossegue.
POST /e-signature/api/v1/esignature/requests/:id/resend
Request (corpo opcional)
{ "signerEmail": "maria@exemplo.com.br" }{ "signerEmail": "maria@exemplo.com.br" }Sem signerEmail, reenvia a todos. Com ele, o signatário precisa existir no envelope — caso contrário, 404.
Resposta 200
{ "message": "Signature request resent successfully" }{ "message": "Signature request resent successfully" }A mensagem confirma que a chamada ao provedor não falhou. Ela não confirma que um e-mail saiu — no OpenSign o reenvio é um efeito colateral de atualizar o documento, não um contrato de envio (§7).
GET /e-signature/api/v1/esignature/requests/:id/download
Resposta 200
{
"downloadUrl": "https://opensign.suaempresa.com.br/files/...",
"expiresAt": "2026-08-16T15:03:11.000Z"
}{
"downloadUrl": "https://opensign.suaempresa.com.br/files/...",
"expiresAt": "2026-08-16T15:03:11.000Z"
}Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Document is not yet completed and signed — a solicitação não está COMPLETED |
404 | NOT_FOUND | Solicitação inexistente, ou o provedor não expôs documento assinado |
A URL é do provedor e o serviço não faz proxy do binário. O expiresAt é uma hora por padrão quando o provedor não informa validade — é uma estimativa nossa, não uma garantia dele. Baixe e guarde no file-storage em vez de reter a URL.
POST /e-signature/api/v1/esignature/webhooks/:configId
Rota pública, chamada pelo provedor. O :configId no caminho identifica a configuração e, por ela, a organização.
O fluxo é: verificar a assinatura do webhook → registrar o evento cru em esignature_webhook_events → aplicar a mudança de estado no envelope e no signatário → marcar o evento como processado. Falha no processamento é gravada no campo error do evento, e o evento permanece registrado.
flowchart TD
W["POST /webhooks/:configId<br/>chamada pelo provedor, sem JWT"] --> V{"assinatura do webhook verificável?"}
V -->|"não, e enforcement = off (padrão)"| Log["aceita e registra aviso no log"]
V -->|"não, e enforcement = on"| Rec["recusa o evento"]
V -->|sim| Log
Log --> G["grava o evento cru em esignature_webhook_events<br/>payload com o corpo original"]
G --> T{"eventType"}
T -->|viewed| E1["envelope de SENT ou DRAFT para VIEWED<br/>signatário para VIEWED, por e-mail"]
T -->|signed| E2["signatário para SIGNED<br/>envelope para COMPLETED ou PARTIALLY_SIGNED"]
T -->|completed| E3["envelope para COMPLETED e completedAt"]
T -->|declined| E4["signatário e envelope para DECLINED"]
T -->|"qualquer outro tipo"| E5["registrado e ignorado"]
E1 --> M["marca o evento como processed"]
E2 --> M
E3 --> M
E4 --> M
E5 --> M
M -.->|"se algo falhar"| Err["grava o motivo em error<br/>e mantém o evento registrado"]Eventos tratados: viewed, signed, completed, declined. Outros tipos são registrados e ignorados.
Esta rota não está funcional hoje — ver §15. Não a inclua no seu desenho de integração antes de confirmar o estado dela.
Início rápido
Do zero a uma solicitação de assinatura criada. Comandos escritos para staging; não executados nesta redação. Você precisa de uma instância OpenSign alcançável.
O caminho tem seis passos, e o terceiro é o que economiza tempo:
flowchart LR
P1["1. Autenticar no IAM"] --> P2["2. Configurar o provedor"]
P2 --> P3["3. Testar a conexão"]
P3 -->|"success: false"| Pare["Pare aqui.<br/>A criação não valida credencial"]
P3 -->|"success: true"| P4["4. Criar a solicitação"]
P4 --> P5["5. Acompanhar por requestId"]
P5 --> P6["6. Listar por businessId"]1. Autenticar no IAM
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://esignature.bb.stg.catalisa.app/e-signature/api/v1/esignatureTOKEN=$(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://esignature.bb.stg.catalisa.app/e-signature/api/v1/esignature2. Configurar o provedor
CFG=$(curl -s -X POST "$BASE/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "OpenSign Homologação",
"providerType": "OPENSIGN",
"credentials": {
"baseUrl": "https://opensign.exemplo.internal",
"appId": "opensign",
"apiPrefix": "/api/app",
"masterKey": "SUBSTITUA_PELO_SEGREDO_DA_SUA_INSTANCIA"
},
"isDefault": true
}')
CFG_ID=$(echo "$CFG" | jq -r '.data.id')
echo "$CFG" | jq '.data.attributes'CFG=$(curl -s -X POST "$BASE/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "OpenSign Homologação",
"providerType": "OPENSIGN",
"credentials": {
"baseUrl": "https://opensign.exemplo.internal",
"appId": "opensign",
"apiPrefix": "/api/app",
"masterKey": "SUBSTITUA_PELO_SEGREDO_DA_SUA_INSTANCIA"
},
"isDefault": true
}')
CFG_ID=$(echo "$CFG" | jq -r '.data.id')
echo "$CFG" | jq '.data.attributes'A resposta traz name, providerType, isDefault e isActive — e nenhum vestígio das credenciais. É o comportamento esperado.
3. Testar a conexão antes de qualquer outra coisa
curl -s -X POST "$BASE/provider-configs/$CFG_ID/test" \
-H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "$BASE/provider-configs/$CFG_ID/test" \
-H "Authorization: Bearer $TOKEN" | jq{ "success": true, "message": "Connection successful" }{ "success": true, "message": "Connection successful" }Se voltar success: false ou um erro, pare aqui. A criação não valida credencial, e seguir adiante só adia a descoberta do problema.
4. Criar a solicitação de assinatura
REQ=$(curl -s -X POST "$BASE/requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"title": "Contrato de Teste",
"message": "Documento de homologação. Não possui valor contratual.",
"fileBase64": "'"$(base64 -w0 contrato.pdf)"'",
"fileName": "contrato.pdf",
"businessId": "teste-001",
"expiresInDays": 7,
"signers": [
{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 }
]
}')
REQ_ID=$(echo "$REQ" | jq -r '.data.id')
echo "$REQ" | jq '{status: .data.attributes.status,
signatarios: [.data.relationships.signers.data[].attributes | {email, status, signingUrl}]}'REQ=$(curl -s -X POST "$BASE/requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"title": "Contrato de Teste",
"message": "Documento de homologação. Não possui valor contratual.",
"fileBase64": "'"$(base64 -w0 contrato.pdf)"'",
"fileName": "contrato.pdf",
"businessId": "teste-001",
"expiresInDays": 7,
"signers": [
{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 }
]
}')
REQ_ID=$(echo "$REQ" | jq -r '.data.id')
echo "$REQ" | jq '{status: .data.attributes.status,
signatarios: [.data.relationships.signers.data[].attributes | {email, status, signingUrl}]}'Resposta prevista:
{
"status": "DRAFT",
"signatarios": [
{ "email": "maria@exemplo.com.br", "status": "SENT", "signingUrl": "https://opensign.exemplo.internal/..." }
]
}{
"status": "DRAFT",
"signatarios": [
{ "email": "maria@exemplo.com.br", "status": "SENT", "signingUrl": "https://opensign.exemplo.internal/..." }
]
}O signingUrl é a URL individual de assinatura. Trate-a como segredo — quem tem o link consegue assinar.
5. Acompanhar
curl -s "$BASE/requests/$REQ_ID" -H "Authorization: Bearer $TOKEN" \
| jq '{status: .data.attributes.status,
signatarios: [.data.relationships.signers.data[].attributes | {email, status, viewedAt, signedAt}]}'curl -s "$BASE/requests/$REQ_ID" -H "Authorization: Bearer $TOKEN" \
| jq '{status: .data.attributes.status,
signatarios: [.data.relationships.signers.data[].attributes | {email, status, viewedAt, signedAt}]}'Resposta prevista, antes de qualquer movimentação do signatário:
{
"status": "DRAFT",
"signatarios": [
{ "email": "maria@exemplo.com.br", "status": "SENT", "viewedAt": null, "signedAt": null }
]
}{
"status": "DRAFT",
"signatarios": [
{ "email": "maria@exemplo.com.br", "status": "SENT", "viewedAt": null, "signedAt": null }
]
}Se o estado nunca mudar depois de a pessoa assinar, não é bug do seu lado: é a rota de webhook, que hoje não está funcional (§15).
6. Listar por operação
curl -s "$BASE/requests?filter%5BbusinessId%5D=teste-001&page%5Bsize%5D=10" \
-H "Authorization: Bearer $TOKEN" | jq '.meta, [.data[].attributes.title]'curl -s "$BASE/requests?filter%5BbusinessId%5D=teste-001&page%5Bsize%5D=10" \
-H "Authorization: Bearer $TOKEN" | jq '.meta, [.data[].attributes.title]'Resposta prevista:
{ "totalItems": 1, "totalPages": 1, "currentPage": 1, "itemsPerPage": 10 }{ "totalItems": 1, "totalPages": 1, "currentPage": 1, "itemsPerPage": 10 }[ "Contrato de Teste" ][ "Contrato de Teste" ]O
contrato.pdfprecisa caber no limite de 1 MB do corpo da requisição, o que na prática significa um PDF de até cerca de 750 KB depois da codificação em base64. Para arquivos maiores, usefileId(§11). Credenciais de staging conforme AMBIENTES.md.
Receitas
Assinar um documento gerado pelo Document Template
A cadeia completa: gerar a CCB, guardar o PDF e mandar assinar. São três building blocks e um único fio ligando os três — o businessId.
flowchart LR
DT["document-template<br/>renderiza a CCB da versão publicada"] -->|"PDF em base64"| FS["file-storage<br/>guarda o arquivo e devolve fileId"]
FS -->|fileId| ES["e-signature<br/>cria o envelope e os signatários"]
ES -->|"signingUrl por signatário"| P["provedor de assinatura"]
BID["businessId = proposta-2026-000481"] -.-> DT
BID -.-> ES1. Gerar o documento — ver o README do document-template.
PDF_B64=$(curl -s -X POST \
"https://document-template.bb.stg.catalisa.app/document-templates/api/v1/document-templates/$TPL_ID/render/pdf" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables": { ... }, "businessId":"proposta-2026-000481"}' \
| jq -r '.data.attributes.pdfBase64')PDF_B64=$(curl -s -X POST \
"https://document-template.bb.stg.catalisa.app/document-templates/api/v1/document-templates/$TPL_ID/render/pdf" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables": { ... }, "businessId":"proposta-2026-000481"}' \
| jq -r '.data.attributes.pdfBase64')O retorno é o PDF em base64. Confira que a variável não ficou vazia antes de seguir — a rota de PDF do document-template não está configurada em staging nem em produção hoje.
2. Guardar no file-storage e obter um fileId
FILE_ID=$(curl -s -X POST https://storage.bb.stg.catalisa.app/file-storage/api/v1/files \
-H "Authorization: Bearer $TOKEN" -F "file=@ccb.pdf" | jq -r '.data.id')
echo "$FILE_ID"FILE_ID=$(curl -s -X POST https://storage.bb.stg.catalisa.app/file-storage/api/v1/files \
-H "Authorization: Bearer $TOKEN" -F "file=@ccb.pdf" | jq -r '.data.id')
echo "$FILE_ID"Resposta prevista: um UUID, que é o que o e-signature aceita em fileId.
8f2c1a90-0000-0000-0000-0000000000008f2c1a90-0000-0000-0000-0000000000003. Mandar assinar, correlacionando pelo mesmo businessId
curl -s -X POST "$BASE/requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"title": "CCB - Proposta 2026-000481",
"fileId": "'"$FILE_ID"'",
"businessId": "proposta-2026-000481",
"signers": [{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 }]
}' | jq '.data.id'curl -s -X POST "$BASE/requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"title": "CCB - Proposta 2026-000481",
"fileId": "'"$FILE_ID"'",
"businessId": "proposta-2026-000481",
"signers": [{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 }]
}' | jq '.data.id'Resposta prevista: o identificador da solicitação de assinatura, que você guarda junto da sua operação.
3c8a1e77-0000-0000-0000-0000000000003c8a1e77-0000-0000-0000-000000000000Armadilhas.
- Use o mesmo
businessIdnos três building blocks. É ele que permite reconstruir a operação depois. - A rota de PDF do document-template não está configurada em staging nem em produção hoje. Verifique antes de montar esse fluxo.
- Prefira
fileIdafileBase64sempre que o arquivo já estiver na plataforma: evita o teto de 1 MB do corpo e evita trafegar o contrato duas vezes. - Guarde o identificador da solicitação de assinatura junto da sua operação. Sem ele, reencontrar o envelope depende de varrer a lista por
businessId.
Coletar assinatura de dois signatários em ordem
curl -s -X POST "$BASE/requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"title": "Contrato com avalista",
"fileId": "'"$FILE_ID"'",
"signers": [
{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 },
{ "name": "João Souza", "email": "joao@exemplo.com.br", "order": 2 }
]
}' | jqcurl -s -X POST "$BASE/requests" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"title": "Contrato com avalista",
"fileId": "'"$FILE_ID"'",
"signers": [
{ "name": "Maria Souza", "email": "maria@exemplo.com.br", "order": 1 },
{ "name": "João Souza", "email": "joao@exemplo.com.br", "order": 2 }
]
}' | jqArmadilhas.
- A ordem sequencial só é ativada quando algum signatário tem
ordermaior que 1. Com todos em 1 — ou semorder—, o envio é simultâneo. - O
emailé a chave de correlação com os eventos do provedor. Dois signatários com o mesmo e-mail quebram o acompanhamento: o estado será aplicado ao primeiro que a consulta encontrar. - No OpenSign, o contato é reaproveitado por e-mail entre solicitações. Mudar o nome de um signatário que já existe não atualiza o cadastro dele no provedor.
- Sem
widgets, o building block posiciona um campo de assinatura por signatário na primeira página, em coordenadas fixas espaçadas horizontalmente. Para contrato de várias páginas, informewidgets— o padrão quase nunca é onde você quer.
Posicionar campos de assinatura em página específica
{
"widgets": [
{ "type": "signature", "page": 8, "x": 90, "y": 610, "width": 180, "height": 55, "required": true },
{ "type": "date", "page": 8, "x": 300, "y": 610, "width": 120, "height": 30 },
{ "type": "checkbox", "page": 8, "x": 90, "y": 680, "label": "Li e concordo" }
]
}{
"widgets": [
{ "type": "signature", "page": 8, "x": 90, "y": 610, "width": 180, "height": 55, "required": true },
{ "type": "date", "page": 8, "x": 300, "y": 610, "width": 120, "height": 30 },
{ "type": "checkbox", "page": 8, "x": 90, "y": 680, "label": "Li e concordo" }
]
}Armadilhas.
- As coordenadas são as do provedor, e a origem do sistema de coordenadas do OpenSign pode não ser a que você espera. Valide visualmente num documento de teste antes de subir para produção.
- Sem
signerId, o campo é atribuído a todos os signatários. Com dois signatários e um campo, você acaba com dois campos sobrepostos na mesma posição. - O
signerIdesperado é o identificador do contato no provedor, não o UUID doSignerno nosso banco. É a armadilha mais provável desta rota. - Os tipos aceitos pelo nosso schema são
signature,text,dateecheckbox. O OpenSign suporta mais tipos, que não estão expostos aqui.
Trocar a configuração padrão de provedor
Objetivo. Passar a usar uma nova conta no provedor sem interromper os envelopes em aberto.
flowchart LR
A["1. Criar a nova configuração<br/>POST /provider-configs"] --> B["2. Testar ANTES de promover<br/>POST /provider-configs/:id/test"]
B -->|"success: false"| Pare["Pare. Corrija a credencial<br/>e teste de novo"]
B -->|"success: true"| C["3. Promover<br/>POST /provider-configs/:id/set-default"]
C --> D["A antiga é desmarcada na mesma transação"]
D --> E["Envelopes já criados continuam na configuração antiga<br/>— o configId está gravado no envelope"]1. Criar a nova configuração
NEW_CFG=$(curl -s -X POST "$BASE/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"OpenSign Novo","providerType":"OPENSIGN","credentials":{ ... }}')
NEW_ID=$(echo "$NEW_CFG" | jq -r '.data.id')NEW_CFG=$(curl -s -X POST "$BASE/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"OpenSign Novo","providerType":"OPENSIGN","credentials":{ ... }}')
NEW_ID=$(echo "$NEW_CFG" | jq -r '.data.id')2. Testar ANTES de promover
curl -s -X POST "$BASE/provider-configs/$NEW_ID/test" -H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "$BASE/provider-configs/$NEW_ID/test" -H "Authorization: Bearer $TOKEN" | jq{ "success": true, "message": "Connection successful" }{ "success": true, "message": "Connection successful" }3. Promover
curl -s -X POST "$BASE/provider-configs/$NEW_ID/set-default" -H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "$BASE/provider-configs/$NEW_ID/set-default" -H "Authorization: Bearer $TOKEN" | jqA resposta traz a configuração com isDefault: true. As demais foram desmarcadas na mesma transação.
Armadilhas.
set-defaultdesmarca todas as outras na mesma transação. Não há janela com duas padrões, nem com nenhuma.- As solicitações já criadas continuam vinculadas à configuração antiga — o
configIdé gravado no envelope. Não exclua a configuração antiga enquanto houver envelope em aberto nela: cancelar, reenviar e baixar dependem dela. - Excluir a configuração é exclusão lógica. Os envelopes continuam apontando para a linha, e as operações continuam funcionando — mas a configuração some da listagem, o que confunde quem for investigar depois.
Diagnosticar uma solicitação que não sai
Objetivo. Separar problema de configuração, de credencial, de tamanho de arquivo e de provedor não implementado, na ordem que resolve mais rápido.
1. Listar as configurações da organização e ver qual é a padrão ativa
curl -s "$BASE/provider-configs" -H "Authorization: Bearer $TOKEN" \
| jq '[.data[] | {id, name: .attributes.name, tipo: .attributes.providerType,
padrao: .attributes.isDefault, ativa: .attributes.isActive}]'curl -s "$BASE/provider-configs" -H "Authorization: Bearer $TOKEN" \
| jq '[.data[] | {id, name: .attributes.name, tipo: .attributes.providerType,
padrao: .attributes.isDefault, ativa: .attributes.isActive}]'Resposta prevista — é aqui que se descobre que não há nenhuma com padrao: true e ativa: true:
[
{ "id": "…", "name": "OpenSign Homologação", "tipo": "OPENSIGN", "padrao": true, "ativa": true }
][
{ "id": "…", "name": "OpenSign Homologação", "tipo": "OPENSIGN", "padrao": true, "ativa": true }
]2. Exercitar a credencial contra o provedor
curl -s -X POST "$BASE/provider-configs/$CFG_ID/test" -H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "$BASE/provider-configs/$CFG_ID/test" -H "Authorization: Bearer $TOKEN" | jq{ "success": true, "message": "Connection successful" }{ "success": true, "message": "Connection successful" }3. Cruzar o sintoma com a causa
flowchart TD
S{"Qual foi o sintoma?"}
S -->|"404 ProviderConfig na criação"| C1["Nenhuma configuração isDefault e isActive<br/>→ rode set-default numa configuração ativa"]
S -->|"500 com not yet implemented"| C2["providerType diferente de OPENSIGN<br/>→ só o OpenSign funciona, §15"]
S -->|"500 INTERNAL ao criar configuração"| C3["ESIGNATURE_CREDENTIAL_MASTER_KEY ausente<br/>ou fora do formato de 64 hexadecimais"]
S -->|"401 ou 403 vindos do provedor"| C4["Credencial errada ou expirada<br/>→ o teste do passo 2 isola isso"]
S -->|413| C5["Corpo acima de 1 MB<br/>→ use fileId em vez de fileBase64"]
S -->|"estado nunca sai de DRAFT ou SENT"| C6["O webhook não está atualizando<br/>→ limitação da §15, confira o painel do provedor"]Ordem de diagnóstico, da causa mais comum para a mais rara:
| Sintoma | Causa provável | O que fazer |
|---|---|---|
404 ProviderConfig na criação | Nenhuma configuração com isDefault e isActive | Rode set-default numa configuração ativa |
500 com "not yet implemented" | providerType diferente de OPENSIGN | Só o OpenSign funciona (§15) |
500 INTERNAL ao criar configuração | ESIGNATURE_CREDENTIAL_MASTER_KEY ausente ou fora do formato | 64 caracteres hexadecimais; verifique o ambiente |
401 ou 403 vindos do provedor | Credencial errada ou expirada | POST /:id/test isola isso em uma chamada |
413 | Corpo acima de 1 MB | Use fileId em vez de fileBase64 |
Estado nunca muda de DRAFT/SENT | O webhook não está atualizando | É a limitação conhecida da §15; confira o painel do provedor |
Integração com outros building blocks
O E-Signature é o elo do meio da cadeia de documentos da Catalisa. O document-template gera; o E-Signature coleta a assinatura; o data-extraction lê o documento que volta.
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token e define o organizationId que isola configurações e envelopes | Sim |
| File Storage | Origem do documento a assinar (fileId) e destino do assinado | Não |
| Document Template | Gera o documento que será assinado | Não |
| Data Extraction | Lê o documento assinado ou o comprovante devolvido pelo cliente | Não |
| Webhooks Engine | Entrega os eventos esignature.* a sistemas externos do cliente | Não |
| Audit Trail | Registra quem criou, cancelou e baixou envelope | Não |
| Decision Platform | Orquestra a esteira que decide, gera o contrato e pede a assinatura | Não |
| Customers | Fornece nome e e-mail dos signatários | Não |
A cadeia de documentos, ponta a ponta
flowchart TD
BID["businessId = proposta-2026-000481<br/>o fio que liga os três"]
DT["Document Template<br/>gera a CCB da versão publicada"]
FS["File Storage<br/>guarda o PDF"]
ES["E-Signature<br/>cria o envelope e os signatários"]
PROV["Provedor de assinatura<br/>OpenSign"]
WH["Webhooks Engine<br/>avisa o sistema do cliente"]
AT["Audit Trail<br/>quem fez o quê"]
DT -->|PDF| FS
FS -->|fileId| ES
ES -->|"URL por signatário"| PROV
ES -->|"eventos esignature.*"| WH
PROV -->|"webhook do provedor"| ES
ES --> AT
BID -.-> DT
BID -.-> ESE na volta, quando o cliente devolve um documento ou manda um comprovante:
flowchart LR
FS2["File Storage<br/>documento recebido"] -->|fileId| DE["Data Extraction<br/>lê e devolve JSON"]
DE -->|"dados estruturados"| Est["de volta à esteira"]Este diagrama é o argumento comercial da Catalisa nesta família: gerar, assinar e ler são o mesmo problema visto de três ângulos. Quem monta essa cadeia com três fornecedores diferentes gasta a maior parte do esforço em amarrar identidade, isolamento por cliente e correlação entre eles — e depois descobre que o relatório "quais operações estão com contrato pendente" exige juntar três bases que não se conhecem.
Aqui as três peças já compartilham o token do IAM, o organizationId do tenant e o businessId da operação. O businessId é o fio: o mesmo identificador de proposta vai no documento gerado, na solicitação de assinatura e na extração do comprovante. Responder "o que já aconteceu com esta operação" é filtrar por um campo em três serviços, e não um projeto de integração.
Vale registrar a fronteira com honestidade: a cadeia está desenhada e as peças se encaixam pelo businessId, mas o encadeamento não é automático — não há orquestração pronta que gere, guarde e envie para assinar em uma chamada. Quem monta o fluxo é a sua aplicação, ou o decision-platform.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
ESIGNATURE_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM para cifrar credenciais de provedor. Exatamente 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32 | Sim, para usar o módulo | — |
DATABASE_URL | PostgreSQL. O building block usa o schema esignature | Sim | — |
REDIS_URL | Redis, usado pelo limite de taxa e pelo publicador de eventos | Sim | — |
JWT_SECRET | Segredo HS256 compartilhado com o IAM. Mínimo 44 caracteres | Sim | — |
S3_* | Endpoint, credenciais e bucket, para resolver fileId | Sim, se usar fileId | — |
PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT | off aceita webhook não verificável com aviso; on recusa | Não | off |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
PORT | Porta em standalone | Não | 3000 (a topologia expõe 3014) |
A chave mestra é lida direto de
process.envno momento da cifragem, e não no boot. Consequência prática: o serviço sobe normalmente sem ela e só falha na primeira operação que precise cifrar ou decifrar, comINTERNAL. Confira a presença dela no deploy; não confie na subida limpa como sinal.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema esignature — configurações, envelopes, signatários, documentos e eventos |
| Redis | Limite de taxa e publicação de eventos |
| S3 ou MinIO | Resolver fileId em conteúdo, via URL assinada |
| IAM | Verificação do token; o organizationId vem do claim assinado |
| Instância do provedor | Hoje, uma instalação OpenSign alcançável pelo serviço |
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
| Corpo da requisição | 1 MB | applyCommonMiddleware — é o teto prático de fileBase64 |
| Espera por chamada ao OpenSign | 30 segundos | Cliente HTTP do provider |
| Upload ao OpenSign | 100 MB | Limite do cliente de upload, inalcançável pelo teto de 1 MB da API |
title | 255 caracteres | Zod |
message | 2.000 caracteres | Zod |
expiresInDays | 1 a 365 | Zod |
| Signatários | mínimo 1, sem teto declarado | Zod |
| Limite de taxa global | 10.000 requisições por minuto por IP, quando ligado | RATE_LIMIT_GLOBAL_MAX |
Eventos publicados
| Evento | Quando |
|---|---|
esignature.request.created | Solicitação criada, por qualquer das duas rotas de criação |
esignature.request.cancelled | Cancelamento concluído |
esignature.request.completed | Webhook completed processado |
esignature.request.declined | Webhook declined processado |
esignature.signer.viewed | Webhook viewed processado |
esignature.signer.signed | Webhook signed processado |
Os quatro últimos dependem do webhook do provedor (§15). esignature.request.viewed e esignature.request.signed existem no catálogo de tipos mas não são publicados por nenhum caminho de código.
Catálogo de erros
O envelope de erro é { "error": "<código>", "message": "<texto>", "details": <opcional> }.
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado, origem de arquivo ambígua, estado incompatível com a operação | Leia a message |
401 | — | Token ausente, inválido ou expirado | Renove no IAM |
403 | — | Falta a permissão exigida, ou o token não carrega organizationId | Confira o papel e a organização |
404 | NOT_FOUND | Configuração, solicitação ou signatário inexistente, excluído, ou de outra organização | Confira o identificador. "De outra organização" e "inexistente" são a mesma resposta, de propósito |
409 | CONFLICT | Nome de configuração repetido na organização | Escolha outro |
413 | — | Corpo acima de 1 MB | Use fileId |
429 | — | Limite de taxa estourado | Aplique recuo exponencial |
500 | INTERNAL | Provedor não implementado; chave mestra ausente; falha ao baixar do S3; erro do provedor | Verifique a message e os logs |
Erros vindos do provedor são traduzidos: 404 vira NOT_FOUND, 401 vira UNAUTHORIZED, 403 vira FORBIDDEN, 400 e 422 viram VALIDATION, 409 vira CONFLICT, e o resto vira INTERNAL com o contexto na mensagem.
Observabilidade.
GET /e-signature/healthresponde a sonda de disponibilidade. É a rota usada pelohealthcheckda pilha.POST /provider-configs/:id/testé a sonda que importa de verdade: ela exercita a credencial contra o provedor. Rode-a periodicamente — credencial expirada é a falha mais comum e a mais silenciosa.- Todo webhook recebido é gravado em
esignature_webhook_events, processado ou não, com o corpo original empayloade o motivo da falha emerror. É a melhor fonte para investigar divergência de estado. Não há rota que a exponha (§15); o acesso é por consulta ao banco. - Assinatura de webhook não verificada gera aviso no log com o nome do provedor e o motivo.
Segurança e compliance
Isolamento entre tenants
Isolamento entre tenants. O organizationId vem do claim assinado do JWT e nunca do corpo. As quinze rotas autenticadas passam pelo middleware local requireOrganization, que devolve 403 quando o claim está ausente. Toda consulta de repositório inclui organizationId na cláusula WHERE, junto com deletedAt: null. Um envelope de outra organização responde 404, não 403: a distinção permitiria descobrir a existência do recurso.
A rota de webhook é a única exceção, e por construção: o provedor não tem token da plataforma. Ela deriva a organização a partir do :configId do caminho, e não de nada que o chamador afirme sobre si. Essa rota deve ficar atrás de lista de IPs permitidos na borda — ver a §15 sobre o estado atual dela.
Como as credenciais do provedor são protegidas
Como as credenciais do provedor são protegidas. Esta é a parte que a ESIGNATURE_CREDENTIAL_MASTER_KEY resolve.
sequenceDiagram
autonumber
participant Cli as Cliente
participant ES as E-Signature
participant PG as PostgreSQL
participant Prov as Provedor
Cli->>ES: POST /provider-configs com credentials em claro
ES->>ES: serializa em JSON
ES->>ES: sorteia um IV de 12 bytes
ES->>ES: cifra em AES-256-GCM com a chave mestra do serviço
ES->>PG: grava no formato iv:authTag:ciphertext, tudo em hexadecimal
ES-->>Cli: 201 SEM o campo credentials — não existe caminho que o exponha
Note over ES,Prov: mais tarde, em cada operação real
ES->>PG: lê o valor cifrado
ES->>ES: decifra em memória — GCM detecta qualquer alteração do texto cifrado
ES->>Prov: usa a credencial e a descartaO que você envia em credentials — a URL da instância, a chave mestra do Parse Server ou o token de sessão — é serializado em JSON e cifrado com AES-256-GCM antes de tocar o banco. O vetor de inicialização tem 12 bytes e é sorteado a cada operação; a etiqueta de autenticação tem 16 bytes. O valor gravado tem o formato iv:authTag:ciphertext, todos em hexadecimal, e o modo GCM garante que qualquer alteração do texto cifrado seja detectada na decifragem em vez de produzir lixo silencioso.
A chave mestra é validada como exatamente 64 caracteres hexadecimais — 32 bytes, o tamanho de uma chave de 256 bits. Ela é do serviço, não do tenant: uma única chave cifra as credenciais de todas as organizações. Isso a torna um ativo crítico, e a consequência operacional é direta: guarde-a em SOPS, nunca em .env versionado, e trate um vazamento dela como comprometimento das credenciais de todos os clientes. Não há rotação de chave implementada — trocar a chave hoje inutiliza todas as credenciais já gravadas.
O campo cifrado nunca volta em nenhuma resposta. A função que monta o corpo de resposta da configuração simplesmente não inclui credentials, então não existe caminho de serialização que o exponha, nem por acidente. Também não há rota que devolva as credenciais em claro para conferência — se você precisa saber qual credencial está gravada, a resposta é rotacioná-la.
Qual nível de assinatura este building block produz
Qual nível de assinatura este building block produz. A resposta honesta começa por uma distinção: o building block não produz assinatura nenhuma. Ele orquestra o provedor configurado, e o nível é o que aquele provedor entrega.
Com o único provedor implementado hoje — o OpenSign auto-hospedado —, o nível produzido é assinatura eletrônica simples. Essa conclusão não vem do material de divulgação do projeto, que não se pronuncia sobre conformidade legal; ela vem da leitura do código de assinatura dele, e a razão é específica: o certificado usado para selar o PDF é do operador do servidor, não do signatário. É um selo de servidor, o mesmo para todos os documentos daquela instalação.
Isso importa porque a lei exige o contrário para os níveis superiores. O art. 6º, parágrafo único da MP nº 2.200-2/2001 determina que "o par de chaves criptográficas será gerado sempre pelo próprio titular e sua chave privada de assinatura será de seu exclusivo controle, uso e conhecimento". E o art. 4º, II, "b" da Lei nº 14.063/2020 exige, para a assinatura avançada, que o signatário opere sob controle exclusivo dos dados de criação. Um selo de servidor não atende nenhum dos dois — mesmo que o operador instale um certificado ICP-Brasil na instalação. Vale dizer isso com todas as letras porque é uma confusão comum e cara.
flowchart TD
Q["De quem é a chave privada que sela o documento?"] --> S["Do operador do servidor<br/>selo de servidor, o mesmo para todos os documentos"]
Q --> T["Do próprio signatário, sob controle exclusivo dele"]
S --> N1["Nível I — assinatura simples<br/>é o que o OpenSign entrega hoje"]
T --> N2["Nível II — avançada<br/>art. 4º, II, b da Lei 14.063/2020"]
T --> N3["Nível III — qualificada<br/>certificado ICP-Brasil, MP 2.200-2, art. 10, § 1º"]
S -.->|"instalar um certificado ICP-Brasil no servidor NÃO muda isso"| N1
MP["MP 2.200-2/2001, art. 6º, parágrafo único:<br/>a chave privada é de controle exclusivo do titular"] -.-> TOs três níveis, com o texto da lei
Os três níveis, com o texto da lei
O art. 4º da Lei nº 14.063/2020 classifica:
| Nível | Definição legal, resumida | O OpenSign atende? |
|---|---|---|
| I — simples | Permite identificar o signatário e anexa ou associa dados a outros dados eletrônicos do signatário | Sim |
| II — avançada | Usa certificado não-ICP ou outro meio de comprovação, admitido pelas partes, com associação unívoca ao signatário, dados sob controle exclusivo dele e detecção de qualquer modificação posterior | Não — falta o controle exclusivo e falta a validação de identidade que o Decreto nº 10.543/2020, art. 5º, II exige |
| III — qualificada | Usa certificado digital nos termos do art. 10, § 1º da MP nº 2.200-2 — ou seja, ICP-Brasil | Não |
Uma nuance de escopo que quase todo material de mercado erra
Uma nuance de escopo que quase todo material de mercado erra. A Lei nº 14.063/2020 rege, pelo art. 1º, assinaturas "em interações com entes públicos, em atos de pessoas jurídicas e em questões de saúde" — e o art. 2º, parágrafo único exclui expressamente processos judiciais e a interação entre particulares. A classificação em três níveis virou a referência técnica de fato do setor inteiro, e é assim que a usamos aqui, mas ela não rege, por si, um contrato privado entre duas empresas. Esse contrato segue a MP nº 2.200-2 e o Código Civil.
O que sustenta a assinatura simples, e qual é o preço dela
O que sustenta a assinatura simples, e qual é o preço dela. O art. 10 da MP nº 2.200-2 tem dois parágrafos que precisam ser lidos juntos:
- O § 1º dá aos documentos assinados com certificação da ICP-Brasil presunção legal de veracidade em relação aos signatários.
- O § 2º ressalva que a MP "não obsta a utilização de outro meio de comprovação da autoria e integridade de documentos em forma eletrônica, inclusive os que utilizem certificados não emitidos pela ICP-Brasil, desde que admitido pelas partes como válido ou aceito pela pessoa a quem for oposto o documento".
| Com certificação ICP-Brasil (§ 1º) | Sem ICP-Brasil, meio aceito pelas partes (§ 2º) | |
|---|---|---|
| Efeito legal | Presunção de veracidade em relação aos signatários | Válido entre as partes que o admitiram |
| Quem tem o ônus da prova | Quem contesta precisa desconstituir | Quem invoca precisa provar autoria e integridade |
| É o que este building block entrega hoje? | Não | Sim |
A diferença entre os dois é o ônus da prova. Com ICP-Brasil, presume-se verdadeiro e cabe a quem contesta desconstituir. Sem ICP-Brasil, quem invoca o documento precisa provar autoria e integridade. Por isso o material comercial da Catalisa não pode dizer que a assinatura simples "tem a mesma validade jurídica". A formulação correta é: é juridicamente válida entre as partes que a admitiram, com ônus probatório distinto.
O ativo legal que muda a conversa: o art. 784, § 4º do CPC
O ativo legal que muda a conversa: o art. 784, § 4º do CPC. A Lei nº 14.620/2023, art. 34, acrescentou ao Código de Processo Civil:
"§ 4º Nos títulos executivos constituídos ou atestados por meio eletrônico, é admitida qualquer modalidade de assinatura eletrônica prevista em lei, dispensada a assinatura de testemunhas quando sua integridade for conferida por provedor de assinatura."
Ou seja: assinatura simples serve para constituir título executivo eletrônico, e as duas testemunhas do art. 784, III caem — desde que a integridade seja conferida pelo provedor de assinatura. Essa condição é técnica e é onde o produto precisa entregar: hash do documento, trilha e detecção de adulteração.
E a CCB, especificamente
E a CCB, especificamente. A Cédula de Crédito Bancário não precisa de testemunhas nem de assinatura qualificada. Ela é título executivo por força de lei própria — art. 28 da Lei nº 10.931/2004 —, enquadrando-se no art. 784, XII do CPC ("os demais títulos aos quais, por disposição expressa, a lei atribuir força executiva"), e não no inciso III, que é o das testemunhas. O art. 29, VI exige apenas a assinatura do emitente, e o § 5º do mesmo artigo, incluído pela Lei nº 13.986/2020, admite que ela seja eletrônica "desde que garantida a identificação inequívoca de seu signatário". A Lei nº 13.986/2020 também acrescentou o art. 27-A, que autoriza a emissão da CCB sob forma escritural.
A trilha da assinatura, e o que este building block preserva hoje
A trilha da assinatura, e o que este building block preserva hoje.
flowchart LR
subgraph Op["Trilha operacional — guardada aqui"]
O1["quem foi convidado: nome e e-mail"]
O2["a ordem exigida entre os signatários"]
O3["quando cada um recebeu, viu, assinou ou recusou"]
O4["todo webhook recebido, com o corpo original"]
end
subgraph Pr["Trilha probatória — fica no provedor"]
P1["hash SHA-256 do PDF assinado"]
P2["trilha de auditoria com IP por evento"]
P3["certificado de conclusão"]
end
Op -->|"serve para você saber e demonstrar internamente"| Uso["acompanhamento e auditoria interna"]
Pr -->|"é o que sustentaria o art. 784, § 4º do CPC"| Prova["prova de integridade"]
Pr -.->|"NÃO é espelhada no nosso banco — §15"| Op| O que o building block guarda | Onde |
|---|---|
| Quem foi convidado a assinar — nome e e-mail | signers |
| A ordem exigida entre os signatários | signers.order |
| Quando cada um recebeu, visualizou, assinou ou recusou | signers.sentAt, viewedAt, signedAt, declinedAt |
| O motivo da recusa, quando o provedor o informa | signers.declineReason |
| Todo evento recebido do provedor, com o corpo original | esignature_webhook_events.payload |
| O vínculo com o documento e com a operação | signature_documents, signature_requests.businessId |
E o que o provedor produz mas o building block não captura — e esta é a lacuna mais relevante deste documento:
| Evidência | O OpenSign produz? | O building block captura? |
|---|---|---|
| Hash SHA-256 do documento assinado | Sim — calculado sobre o PDF final e persistido do lado dele | Não. Nenhuma coluna do nosso schema guarda resumo criptográfico |
| Trilha de auditoria com endereço IP por evento | Sim — por usuário, atividade, IP e horário | Não. Não há mapeamento, e a interface de provedor não tem método para buscá-la |
| Certificado de conclusão | Sim — PDF com identificador, hash, organização, datas e, por signatário, nome, e-mail, IP e horário | Não. Não é buscado nem armazenado |
| Carimbo de tempo confiável (RFC 3161) | Não — os horários são do relógio do servidor, sem terceiro de confiança | Não se aplica |
| Autenticação adicional do signatário por código | Existe no OpenSign | Não é ativado pelo nosso provider — o signatário é autenticado apenas pela posse do link |
A leitura prática, e ela é dura: a trilha que este building block mantém hoje é operacional — serve para você saber e demonstrar internamente o que aconteceu. A trilha probatória, a que sustentaria a condição do art. 784, § 4º, existe do lado do OpenSign e não está sendo espelhada. Enquanto isso não for corrigido (§15), a prova de integridade depende de recorrer diretamente ao provedor. Não afirme a cliente que o building block, por si, confere integridade verificável.
O que dizer e o que não dizer a um cliente
O que dizer e o que não dizer a um cliente
| Situação | A assinatura simples basta? |
|---|---|
| Contrato entre partes privadas que aceitaram o meio eletrônico | Em geral sim, com base no art. 10, § 2º da MP nº 2.200-2 — com ônus probatório de quem invoca o documento |
| Constituir título executivo eletrônico | Sim, pelo art. 784, § 4º do CPC — desde que a integridade seja conferida pelo provedor |
| Emitir CCB | Sim — a lei não exige testemunha nem ICP-Brasil, apenas identificação inequívoca do signatário |
| Emissão de nota fiscal eletrônica, salvo pessoa física e MEI | Não — o art. 5º, § 2º, III da Lei nº 14.063 exige qualificada |
| Ato de transferência e registro de bem imóvel | Não, em regra — o art. 5º, § 2º, IV exige qualificada. A Lei nº 14.382/2022 abriu caminho para a avançada em registros públicos, condicionado a ato do CNJ |
| Receituário de medicamento sob controle especial e atestado médico | Não — o art. 13 da Lei nº 14.063 exige qualificada |
| Ato em que a autoria pode ser contestada em juízo | Depende inteiramente da robustez da prova que o provedor produz |
Se o seu caso exige assinatura avançada ou qualificada, o caminho é contratar um provedor que a ofereça — e hoje isso significa não usar este building block, porque nenhum provedor com suporte a ICP-Brasil está implementado (§15). Nunca afirme a cliente que este building block confere validade jurídica. Ele confere rastreabilidade operacional; a validade decorre do provedor, do meio aceito pelas partes e do ato praticado. Consulte o jurídico antes de usá-lo em ato de maior exigência.
Nota de método: as citações desta seção foram conferidas contra o texto das normas em 2026-08-16. Circula no mercado jurisprudência do STJ sobre contrato eletrônico como título executivo sem testemunhas; não conseguimos verificar nenhum acórdão em fonte primária e por isso não citamos nenhum. Não use precedente de segunda mão em material comercial.
Dados pessoais, exclusão lógica e superfície de exposição
Dados pessoais. O building block guarda nome e e-mail de cada signatário, o título do documento e o vínculo com a operação. O conteúdo do documento não é armazenado aqui — ele fica no file-storage e no provedor. E-mail e nome são dados pessoais sob a LGPD. O envio ao provedor é uma operação de tratamento por operador: se o provedor for de nuvem, e especialmente se estiver no exterior, isso exige base legal, contrato de operador e atenção às regras de transferência internacional. Com provedor auto-hospedado, esse trânsito não existe.
Exclusão lógica. Configurações e solicitações usam deletedAt. Excluir não apaga signatários, documentos nem eventos de webhook — o histórico sobrevive à remoção, que é o que auditoria exige. Atender a pedido de eliminação da LGPD exige expurgo manual (§15).
Superfície de exposição. O signingUrl de cada signatário e o downloadUrl do documento assinado são URLs com poder: a primeira permite assinar, a segunda permite ler o contrato assinado. Elas aparecem em respostas da API e não devem ir para log de aplicação, para sistema de tíquete nem para mensagem de chat.
Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
| Só o OpenSign está implementado | DOCUSIGN, ADOBESIGN e ZAPSIGN existem no enum, são aceitos na criação da configuração e falham com INTERNAL na primeira operação real, com a mensagem "not yet implemented". A camada de abstração está pronta; as implementações, não | Roadmap — não anuncie multi-provedor como capacidade disponível |
| A rota de webhook não está funcional | A rota resolve a configuração com escopo de organização vazio e, por isso, responde 404 a todo evento. Na prática nenhum webhook é processado, e o estado do envelope não avança sozinho a partir do que foi gravado na criação | Bloqueante para acompanhamento automático — corrigir é prioridade |
| Não há rota para reconsultar o estado no provedor | O provider implementa getSignatureRequest, mas nenhuma rota o expõe. Com o webhook indisponível, não há caminho pela API para atualizar o estado | Roadmap — é a mitigação natural do item anterior |
| O OpenSign não assina webhooks | Ele não oferece esquema de assinatura, então a verificação nunca é real. Com PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=on, todo webhook do OpenSign é recusado; com off, é aceito com aviso. A mitigação correta é lista de IPs na borda | Limitação do provedor |
| Nenhum provedor com ICP-Brasil | O nível de assinatura obtido hoje é o simples. Atos que exijam assinatura qualificada não são atendidos (§14) | Depende de implementar um provedor que suporte |
| As evidências probatórias do provedor não são capturadas | O OpenSign produz hash SHA-256 do documento, trilha de auditoria com IP e certificado de conclusão. Nenhum dos três é buscado nem armazenado — não há coluna no schema, e a interface de provedor não tem método para obtê-los. É o que sustentaria a condição do art. 784, § 4º do CPC (§14) | Prioridade alta — os dados já existem do lado do provedor, falta mapeá-los |
| A autenticação adicional do signatário não é ativada | O OpenSign oferece verificação por código de uso único; o nosso provider nunca a liga. O signatário é autenticado apenas pela posse do link enviado por e-mail — o degrau mais fraco possível | Roadmap |
| O nível de assinatura não é modelado | Não há campo que registre qual nível da Lei nº 14.063 foi produzido em cada documento. O produto não consegue declarar nem auditar isso por solicitação | Roadmap |
| O OpenSign não emite carimbo de tempo confiável | Os horários da trilha são do relógio do servidor, sem autoridade de carimbo de tempo (RFC 3161). Para prova de anterioridade, isso é fraco | Limitação do provedor |
| A licença do OpenSign não é AGPL-3 pura | O arquivo de licença abre exceção para um diretório cujo termo de licença não existe no repositório, e o GitHub classifica o projeto como NOASSERTION. Somado à cláusula de rede da AGPL-3, exige análise jurídica antes de uso comercial | Pendente de avaliação jurídica |
| Corpo limitado a 1 MB | fileBase64 só comporta arquivo de cerca de 750 KB depois da codificação. O cliente do provedor aceita 100 MB, limite inalcançável pela API. Use fileId | Por design do middleware comum; ajustável na infraestrutura |
| Criar configuração não valida credencial | Uma credencial errada, ou um providerType não implementado, é aceita e só falha depois. Sempre chame POST /:id/test | Por design; o teste existe para isso |
EXPIRED nunca é gravado | O estado existe no enum e é mapeado a partir do provedor, mas nenhum caminho do código o persiste. Envelope vencido continua no último estado conhecido | Roadmap |
| Reenvio é efeito colateral, não contrato | No OpenSign, reenviar é atualizar um campo do documento para disparar a notificação. A resposta de sucesso não garante que um e-mail saiu | Limitação do provedor |
from-template usa template do provedor | A rota POST /requests/from-template consome templates cadastrados na instância do provedor, e não templates do document-template. São coisas diferentes com o mesmo nome | Por design, mas confunde |
| Sem rota para listar templates do provedor | O provider implementa listTemplates, mas nenhuma rota o expõe. Descobrir o templateId exige acessar o painel do provedor | Roadmap |
| Sem rota para ler os eventos de webhook | esignature_webhook_events é gravada mas não exposta. Investigar divergência exige consulta ao banco | Roadmap |
| Sem rotação da chave mestra | Trocar ESIGNATURE_CREDENTIAL_MASTER_KEY inutiliza todas as credenciais já cifradas. Não há procedimento de recifragem | Roadmap |
Permissões só no papel ADMIN | ESIGNATURE_ADMIN não está atribuída a nenhum papel intermediário no catálogo padrão do IAM. O papel DOCUMENT_OPERATOR recebe CREATE, READ e DELETE, mas não ADMIN — ou seja, ele não consegue configurar provedor | Configuração pendente no IAM |
| Sem retenção nem expurgo automatizado | Nome e e-mail de signatário ficam indefinidamente. Atender pedido de eliminação da LGPD é processo manual | Roadmap |
| Sem lembrete configurável | O OpenSign é configurado com lembrete automático a cada 3 dias, valor fixo no código. Não há como ajustar pela API | Roadmap |
Perguntas frequentes
Qual nível de assinatura eu obtenho com este building block?
Assinatura eletrônica simples, com o único provedor implementado hoje. O motivo é técnico e específico: o certificado que sela o PDF no OpenSign é do operador do servidor, não do signatário — e tanto o art. 6º, parágrafo único da MP nº 2.200-2/2001 quanto o art. 4º, II, "b" da Lei nº 14.063/2020 exigem que os dados de criação da assinatura estejam sob controle exclusivo do signatário para caracterizar nível superior. Instalar um certificado ICP-Brasil no servidor não muda isso. Leia a §14 inteira antes de responder a um cliente, e envolva o jurídico se o ato for de maior exigência.
Assinatura simples serve para um contrato virar título executivo?
Sim, e este é o ponto legal mais forte do produto. O art. 784, § 4º do CPC, incluído pela Lei nº 14.620/2023, admite qualquer modalidade de assinatura eletrônica para títulos executivos constituídos por meio eletrônico, e dispensa as duas testemunhas quando a integridade for conferida por provedor de assinatura. A condição é técnica. E aqui vem a ressalva honesta: o OpenSign produz as evidências necessárias — hash SHA-256, trilha com IP, certificado de conclusão —, mas o building block ainda não as captura (§15). Enquanto isso não for corrigido, a prova de integridade depende de recorrer ao provedor.
Preciso de duas testemunhas na CCB?
Não. A CCB é título executivo por força do art. 28 da Lei nº 10.931/2004, enquadrando-se no art. 784, XII do CPC — não no inciso III, que é o dispositivo das testemunhas. O art. 29, VI exige apenas a assinatura do emitente e, se houver, do garantidor, e o § 5º admite que ela seja eletrônica desde que garantida a identificação inequívoca do signatário.
Vocês suportam DocuSign?
Não hoje. O tipo existe no schema e a configuração é aceita, mas a primeira operação real falha com "not yet implemented". A camada de abstração está pronta — uma implementação nova é uma classe que atende oito métodos —, e é isso que está pronto. A capacidade, não. Não anuncie multi-provedor como disponível.
Por que a rota é /e-signature/api/v1/esignature/...?
Porque o basePath do app usa hífen e o ponto de montagem dos routers não usa. É feio e é real. Copie do §9.
O status da minha solicitação não muda. O que houve?
Provavelmente nada do seu lado. A atualização de estado depende do webhook do provedor, e essa rota não está funcional hoje (§15). Também não existe rota para reconsultar o estado no provedor. Enquanto isso não for corrigido, o acompanhamento fino depende do painel do provedor. É a limitação mais importante deste building block e é a primeira coisa a verificar.
Onde ficam as credenciais do provedor?
Cifradas em AES-256-GCM na coluna credentials, com a chave ESIGNATURE_CREDENTIAL_MASTER_KEY. Elas não voltam em nenhuma resposta e não há rota que as devolva em claro. Se você precisa saber o que está gravado, o caminho é rotacionar. A chave mestra é do serviço e cifra as credenciais de todos os tenants — guarde-a em SOPS.
Posso usar fileBase64 para um contrato de 5 MB?
Não. O corpo da requisição é limitado a 1 MB, o que dá cerca de 750 KB de arquivo depois da codificação em base64. Guarde o arquivo no file-storage e use fileId — é o caminho recomendado de qualquer forma, porque evita trafegar o contrato duas vezes.
Como garanto que o documento assinado é o mesmo que enviei?
Hoje, recorrendo ao provedor. O OpenSign calcula e guarda um hash SHA-256 do PDF final assinado e emite um certificado de conclusão com esse hash — mas o building block não busca nem armazena nenhum dos dois (§15). Enquanto essa lacuna existir, calcule e guarde o resumo do arquivo na sua própria aplicação antes de enviar, e trate a consulta ao painel do provedor como parte do procedimento de prova.
O signatário precisa se autenticar de alguma forma além do link?
Não, hoje. O OpenSign oferece verificação por código de uso único, mas o nosso provider não a ativa (§15). Na prática, quem tem o link consegue assinar — inclusive quem receber o e-mail encaminhado. Trate o signingUrl como segredo e considere isso na sua avaliação de risco.
Qual a diferença entre o template daqui e o do Document Template?
São coisas diferentes com o mesmo nome. POST /requests/from-template usa um template cadastrado na instância do provedor de assinatura. O document-template é um building block separado que gera o documento antes de ele chegar aqui. O fluxo normal é: o document-template gera o PDF, você o guarda no file-storage, e o e-signature o envia por fileId.
O que acontece se eu excluir uma configuração com envelopes em aberto?
A exclusão é lógica, então a linha continua no banco e os envelopes continuam funcionando — cancelar, reenviar e baixar seguem operando. O efeito é que a configuração some da listagem, o que atrapalha quem for investigar depois. Evite excluir antes de encerrar os envelopes.
Preciso de uma instância OpenSign própria?
Sim, hoje. É o único provedor implementado e é auto-hospedado. Isso tem um lado bom que vale explicitar no comercial: com ele, o contrato nunca sai da sua infraestrutura, o que elimina uma transferência de dado pessoal a terceiro. E tem o lado a ser dito com clareza: você opera essa instância, e o nível de assinatura obtido é o simples.
Dois signatários podem ter o mesmo e-mail?
Tecnicamente o schema não impede, mas não faça isso. A correlação entre os eventos do provedor e o signatário é feita pelo e-mail — com e-mails repetidos, a atualização de estado vai para o primeiro registro encontrado e o acompanhamento fica errado sem avisar.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md