Catalisa.Building Blocks
Catálogo/Documentos/E-Signature

E-Signature

Beta

Coleta de assinatura eletrônica em documento, com trilha de eventos por signatário

16
Endpoints
5
Entidades
1
Provedores
Tenant
Escopo
3014
Porta
2026-01
Desde

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.

Para quem é
  • 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
Substitui
  • 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
O que não é
  • 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)
O que dá para fazer

17 endpoints em 4 recursos.

Explorar a API →
01

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.

AtributoValor
Identificadore-signature
CategoriaDocumentos
EscopoTenant (exige organizationId no token em todas as rotas, exceto a de webhook)
Porta (standalone)3014
Path alias@e-signature
Prefixo HTTP/e-signature
StatusBeta desde 2026-01
Depende dePostgreSQL (schema esignature), Redis, S3, provedor de assinatura
PermissõesESIGNATURE_ADMIN, ESIGNATURE_CREATE, ESIGNATURE_READ, ESIGNATURE_DELETE

02

O problema

negócio

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


03

Proposta de valor

negócio
AntesDepois
O código chama a API de um provedor específicoO provedor é uma configuração por organização, cifrada no banco
Uma conta do provedor compartilhada por todos os clientesCada organização configura a própria credencial, isolada e rotacionável
"Está enviado" é tudo que você sabeEstado por signatário: pendente, enviado, visualizado, assinado, recusado
Contrato circula como anexo de e-mailO arquivo vem do file-storage por identificador e não trafega a cada chamada
Trocar de fornecedor é reescrever a integraçãoA 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.


04

Casos de uso reais

negócio

Caso 1 — Uma financeira coleta assinatura de devedor e avalista em ordem Cenário ilustrativo

Contexto

Financeira de crédito pessoal com garantia de terceiro. Cada operação exige a assinatura do devedor e, depois dela, a do avalista.

A dor

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.

A solução com o BB

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ário
O resultado

A 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

Contexto

Plataforma B2B que atende dezenas de empresas clientes, cada uma com preferência e contrato próprios com fornecedores de assinatura.

A dor

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.

A solução com o BB

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"]
O resultado

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

Contexto

Operação de originação com cerca de 400 contratos por mês e três pessoas acompanhando o retorno das assinaturas.

A dor

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.

A solução com o BB

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"]
O resultado

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

Contexto

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 dor

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"]
A solução

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 resultado

O caminho legal está aberto e é textualmente ancorado. O que falta é do nosso lado, e está identificado.


05

Mercado e diferenciais

negócio

Panorama: 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érioCatalisa E-SignatureDocuSignClicksignD4SignZapSignAutentiqueOpenSign
NaturezaCamada de orquestraçãoProvedorProvedorProvedorProvedorProvedorProvedor auto-hospedável
Assina documentosNão — delega ao provedorSimSimSimSimSimSim
Entrada publicada (2026-08-16)Em definiçãoR$ 45/mêsR$ 39/mêsR$ 39,90/mêsGrátis até 3 docs; R$ 39,90/mêsGrátis até 10 docs; R$ 99/mêsSem custo por documento
Preço de API públicoR$ 2.560 a R$ 24.520/anoIncluso desde o plano de entradaNão publicadoA partir do plano intermediárioTarifa por evento publicada em reais
Certificado ICP-BrasilDepende do provedorSim, mas só nos planos sob cotaçãoSimSimÉ AC credenciada pelo ITISim — A1, A3 e nuvemNão
Nível da Lei 14.063 que declara produzirO do provedor configuradoNão declara por planoAlega cobrir os trêsNão declaraNão declaraDeclara avançada (art. 4º, II)Não declara — o código indica simples
Troca de provedor sem reescreverSim, é o pontoNão se aplicaNão se aplicaNão se aplicaNão se aplicaNão se aplicaNão se aplica
Credencial isolada por clienteSim, cifrada por organizaçãoVocê implementaVocê implementaVocê implementaVocê implementaVocê implementaVocê implementa
Estado por signatárioSim, tabela própriaSimSimSimSimSimSim
Documento fica na sua infraestruturaSim, se o provedor for auto-hospedadoNãoNãoNãoNãoNãoSim
Assinatura de webhook verificávelDepende do provedor (§15)SimSim, HMACNão documentadoSimSimNã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

  1. 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.
  2. 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.
  3. Envelope e signatário têm estados independentes. SignatureRequest tem oito estados; Signer tem cinco. Isso permite o estado intermediário PARTIALLY_SIGNED, que é onde a maioria dos contratos de crédito com avalista realmente vive.
  4. 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.


06

Modelo de cobrança e ROI

negócio

Quem 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"| Cli

Precificaçã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.

DriverPor quê
Solicitações de assinatura criadasCada 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çãoO provedor cria um contato e um conjunto de campos por signatário, e a maioria cobra por isso
Tamanho do documentoO arquivo é carregado em memória em base64 antes de subir ao provedor
Consultas de estadoEnquanto 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-hospedadoDocuSignClicksignZapSignAutentique
Base de cálculoPrecificação em definição; sem custo por documentoPor usuário emissor, mais franquia de envio por APIPor faixa de documentos, com excedentePor faixa, com faixa gratuita e recursos avulsosDocumentos ilimitados no plano profissional
Ordem de grandeza mensal do provedorSó a infraestrutura da instânciaCentenas de reais por usuário, mais contrato de API a partir de R$ 2.560/anoDezenas a centenas de reais, conforme a faixaDezenas de reais, mais avulsos por recursoR$ 99/mês, mais eventos avulsos
Custo por documento excedenteZeroConsumo de franquiaR$ 2,40 a R$ 6,90Conforme planoTarifa por evento publicada
ICP-Brasil disponívelNãoSim, sob cotaçãoSimSimSim
Você opera o provedorSimNãoNãoNãoNã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 contaO que se ganhaO que ressalvar
Tempo entre aprovar e liquidarDias viram horas; o ganho é proporcional ao ticket, não ao número de contratosDepende de o acompanhamento funcionar — hoje o webhook não está funcional (§15)
Custo de troca de fornecedorA troca vira configuração, e o poder de negociação deixa de ser todo do fornecedorValor 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.


07

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"| R3

Caminho 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
    end

Atençã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 verifyWebhookSignature retornavam true incondicionalmente, 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 de PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT: com off (padrão), o webhook é aceito e um aviso é registrado; com on, 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.create cifra e grava sem chamar o provedor. Uma credencial errada, ou um providerType não implementado, é aceita e só falha na primeira solicitação real. Existe POST /provider-configs/:id/test exatamente 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 grava CANCELLED. 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 respostas 404, 409 e 422 do 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 SentToOthers do 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/download responde com downloadUrl e expiresAt; 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 — o expiresAt padrão de uma hora é um palpite nosso, não uma garantia dele.
  • O arquivo por fileId passa 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.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
Solicitação de assinatura (envelope)A unidade de trabalho: um documento, um conjunto de signatários e um estado. Corresponde ao modelo SignatureRequest.
SignatárioUma 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 provedorO provedor escolhido pela organização e suas credenciais cifradas. Uma delas é a padrão.
externalIdO identificador do recurso no provedor. É por ele que o webhook reencontra a solicitação. Único em conjunto com a configuração.
businessIdIdentificador livre da operação de origem — proposta, contrato, pedido. É o campo de correlação com os outros building blocks.
Chave mestra de credenciaisESIGNATURE_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 simplesNa 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.
OpenSignPlataforma 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 PrismaTabelaPropósitoCampos-chave
ESignatureProviderConfigesignature_provider_configsProvedor e credenciais da organizaçãoÚnico (organizationId, name), providerType, credentials (cifrado), isDefault, isActive, settings, deletedAt
SignatureRequestsignature_requestsO envelopeÚnico (configId, externalId), title, status, businessId, templateId, viewUrl, expiresAt, completedAt, deletedAt
SignersignersUm signatário do envelopeexternalId, email, order, status, signingUrl, sentAt, viewedAt, signedAt, declinedAt, declineReason
SignatureDocumentsignature_documentsO arquivo vinculado ao envelopeexternalId, fileId, originalUrl, signedUrl, signedUrlExpiresAt
ESignatureWebhookEventesignature_webhook_eventsTodo webhook recebido, crueventType, 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

EnumValores
ESignatureProviderTypeOPENSIGN (implementado) · DOCUSIGN · ADOBESIGN · ZAPSIGN (declarados, recusam em execução — §15)
SignatureRequestStatusDRAFT · SENT · VIEWED · PARTIALLY_SIGNED · COMPLETED · DECLINED · EXPIRED · CANCELLED
SignerStatusPENDING · 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 note

Três leituras que o diagrama não diz sozinho:

ObservaçãoConsequência prática
DRAFT e SENT dependem do que o provedor reporta na criaçãoO 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 persisteEnvelope 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 COMPLETED quando todos os signatários estão SIGNED.
  • 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 declineReason existe como coluna, mas o tratamento do webhook declined grava apenas status e declinedAt; o motivo depende do provedor informá-lo por outro caminho.

09

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étodoRotaDescriçãoPermissão
POST/e-signature/api/v1/esignature/provider-configsCria configuração e cifra as credenciais. Responde 201ESIGNATURE_ADMIN
GET/e-signature/api/v1/esignature/provider-configsLista as configurações da organizaçãoESIGNATURE_READ
GET/e-signature/api/v1/esignature/provider-configs/:idBusca uma configuração (sem as credenciais)ESIGNATURE_READ
PATCH/e-signature/api/v1/esignature/provider-configs/:idAtualiza nome, credenciais, ajustes ou estadoESIGNATURE_ADMIN
DELETE/e-signature/api/v1/esignature/provider-configs/:idExclusão lógica. Responde 204ESIGNATURE_ADMIN
POST/e-signature/api/v1/esignature/provider-configs/:id/testTesta a conexão com o provedorESIGNATURE_ADMIN
POST/e-signature/api/v1/esignature/provider-configs/:id/set-defaultMarca como padrão e desmarca as demaisESIGNATURE_ADMIN

Solicitações de assinatura — /e-signature/api/v1/esignature/requests

MétodoRotaDescriçãoPermissão
POST/e-signature/api/v1/esignature/requestsCria a solicitação a partir de um arquivo. Responde 201ESIGNATURE_CREATE
POST/e-signature/api/v1/esignature/requests/from-templateCria a partir de um template do provedor. Responde 201ESIGNATURE_CREATE
GET/e-signature/api/v1/esignature/requestsLista solicitações, paginadoESIGNATURE_READ
GET/e-signature/api/v1/esignature/requests/:idBusca a solicitação com os signatáriosESIGNATURE_READ
POST/e-signature/api/v1/esignature/requests/:id/cancelCancela a solicitaçãoESIGNATURE_CREATE
POST/e-signature/api/v1/esignature/requests/:id/resendReenvia a todos ou a um signatárioESIGNATURE_CREATE
GET/e-signature/api/v1/esignature/requests/:id/downloadURL do documento assinado. Exige COMPLETEDESIGNATURE_READ
DELETE/e-signature/api/v1/esignature/requests/:idExclusão lógica. Responde 204ESIGNATURE_DELETE

Filtros aceitos em GET /requests: filter[status] e filter[businessId]. Paginação por page[number] e page[size].

Webhook do provedor

MétodoRotaDescriçãoAutenticação
POST/e-signature/api/v1/esignature/webhooks/:configIdRecebe evento do provedorPública — sem JWT. Ver §14 e §15

Saúde

MétodoRotaDescrição
GET/e-signature/healthSonda 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

json
{
  "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
}
CampoTipoObrigatórioDescrição
namestring (1–100)SimNome da configuração. Único por organização
providerTypeOPENSIGN | DOCUSIGN | ADOBESIGN | ZAPSIGNSimOPENSIGN funciona hoje (§15)
credentialsobjeto de textosSimCifrado antes de gravar. As chaves dependem do provedor
isDefaultbooleanoNãoSem valor, a primeira configuração da organização vira a padrão
isActivebooleanoNãoPadrão true
settingsobjetoNãoAjustes específicos do provedor

Credenciais do OpenSign

ChaveObrigatóriaDescrição
baseUrlSimURL da instância OpenSign. Sem ela, a criação do provedor falha
masterKeyUma das duasChave mestra do Parse Server. Acesso administrativo — evite em produção
sessionTokenUma das duasToken de sessão de um usuário. Requer também extUserId
extUserIdCom sessionTokenIdentificador do registro contracts_Users do usuário
appIdNãoIdentificador da aplicação Parse. Padrão opensign
apiPrefixNãoPadrã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

StatusCódigoQuando
400VALIDATIONCorpo reprovado no Zod, ou providerType fora do enum
403Sem ESIGNATURE_ADMIN, ou token sem organizationId
409CONFLICTJá existe configuração com esse nome na organização
500INTERNALESIGNATURE_CREDENTIAL_MASTER_KEY ausente ou malformada — a cifragem falha

A criação não testa a conexão. Chame POST /:id/test logo 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

json
{
  "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 }
  ]
}
CampoTipoObrigatórioDescrição
titlestring (1–255)SimTítulo do envelope
messagestring (máx. 2000)NãoMensagem aos signatários
configIdUUIDNãoConfiguração a usar. Omitido, usa a padrão da organização
fileIdUUIDUm dos doisArquivo já guardado no file-storage
fileBase64stringUm dos doisArquivo em base64. Exige fileName
fileNamestring (máx. 255)Com fileBase64Nome do arquivo, usado para deduzir o tipo
signerslista, mínimo 1SimCada item com name, email e order opcional
widgetslistaNãoCampos de assinatura. Sem eles, um campo por signatário na primeira página
businessIdstring (máx. 100)NãoIdentificador da operação de origem
expiresInDaysinteiro 1–365NãoNo OpenSign, vira TimeToCompleteDays; padrão do provedor é 15

Resposta 201

json
{
  "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

StatusCódigoQuando
400VALIDATIONNenhum ou os dois de fileId/fileBase64; fileBase64 sem fileName; lista de signatários vazia
404NOT_FOUNDconfigId inexistente, ou a organização não tem configuração padrão ativa
413Corpo acima de 1 MB — o teto prático de fileBase64 (§15)
500INTERNALproviderType 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

StatusCódigoQuando
400VALIDATIONCannot cancel request with status COMPLETED ou CANCELLED
404NOT_FOUNDSolicitaçã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)

json
{ "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

json
{ "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

json
{
  "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

StatusCódigoQuando
400VALIDATIONDocument is not yet completed and signed — a solicitação não está COMPLETED
404NOT_FOUNDSolicitaçã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.


10

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

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

BASE=https://esignature.bb.stg.catalisa.app/e-signature/api/v1/esignature
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/esignature

2. Configurar o provedor

bash
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

bash
curl -s -X POST "$BASE/provider-configs/$CFG_ID/test" \
  -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/provider-configs/$CFG_ID/test" \
  -H "Authorization: Bearer $TOKEN" | jq
json
{ "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

bash
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:

json
{
  "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

bash
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:

json
{
  "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

bash
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:

json
{ "totalItems": 1, "totalPages": 1, "currentPage": 1, "itemsPerPage": 10 }
{ "totalItems": 1, "totalPages": 1, "currentPage": 1, "itemsPerPage": 10 }
json
[ "Contrato de Teste" ]
[ "Contrato de Teste" ]

O contrato.pdf precisa 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, use fileId (§11). Credenciais de staging conforme AMBIENTES.md.


11

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

1. Gerar o documento — ver o README do document-template.

bash
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

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

texto
8f2c1a90-0000-0000-0000-000000000000
8f2c1a90-0000-0000-0000-000000000000

3. Mandar assinar, correlacionando pelo mesmo businessId

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

texto
3c8a1e77-0000-0000-0000-000000000000
3c8a1e77-0000-0000-0000-000000000000

Armadilhas.

  • Use o mesmo businessId nos 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 fileId a fileBase64 sempre 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

bash
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 }
    ]
  }' | jq
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 }
    ]
  }' | jq

Armadilhas.

  • A ordem sequencial só é ativada quando algum signatário tem order maior que 1. Com todos em 1 — ou sem order —, 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, informe widgets — o padrão quase nunca é onde você quer.

Posicionar campos de assinatura em página específica

json
{
  "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 signerId esperado é o identificador do contato no provedor, não o UUID do Signer no nosso banco. É a armadilha mais provável desta rota.
  • Os tipos aceitos pelo nosso schema são signature, text, date e checkbox. 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

bash
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

bash
curl -s -X POST "$BASE/provider-configs/$NEW_ID/test" -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/provider-configs/$NEW_ID/test" -H "Authorization: Bearer $TOKEN" | jq
json
{ "success": true, "message": "Connection successful" }
{ "success": true, "message": "Connection successful" }

3. Promover

bash
curl -s -X POST "$BASE/provider-configs/$NEW_ID/set-default" -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/provider-configs/$NEW_ID/set-default" -H "Authorization: Bearer $TOKEN" | jq

A resposta traz a configuração com isDefault: true. As demais foram desmarcadas na mesma transação.

Armadilhas.

  • set-default desmarca 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

bash
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:

json
[
  { "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

bash
curl -s -X POST "$BASE/provider-configs/$CFG_ID/test" -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/provider-configs/$CFG_ID/test" -H "Authorization: Bearer $TOKEN" | jq
json
{ "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:

SintomaCausa provávelO que fazer
404 ProviderConfig na criaçãoNenhuma configuração com isDefault e isActiveRode set-default numa configuração ativa
500 com "not yet implemented"providerType diferente de OPENSIGNSó o OpenSign funciona (§15)
500 INTERNAL ao criar configuraçãoESIGNATURE_CREDENTIAL_MASTER_KEY ausente ou fora do formato64 caracteres hexadecimais; verifique o ambiente
401 ou 403 vindos do provedorCredencial errada ou expiradaPOST /:id/test isola isso em uma chamada
413Corpo acima de 1 MBUse fileId em vez de fileBase64
Estado nunca muda de DRAFT/SENTO webhook não está atualizandoÉ a limitação conhecida da §15; confira o painel do provedor

12

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 blockComo se relacionaObrigatório
IAMEmite o token e define o organizationId que isola configurações e envelopesSim
File StorageOrigem do documento a assinar (fileId) e destino do assinadoNão
Document TemplateGera o documento que será assinadoNão
Data ExtractionLê o documento assinado ou o comprovante devolvido pelo clienteNão
Webhooks EngineEntrega os eventos esignature.* a sistemas externos do clienteNão
Audit TrailRegistra quem criou, cancelou e baixou envelopeNão
Decision PlatformOrquestra a esteira que decide, gera o contrato e pede a assinaturaNão
CustomersFornece nome e e-mail dos signatáriosNã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 -.-> ES

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


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
ESIGNATURE_CREDENTIAL_MASTER_KEYChave AES-256-GCM para cifrar credenciais de provedor. Exatamente 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32Sim, para usar o módulo
DATABASE_URLPostgreSQL. O building block usa o schema esignatureSim
REDIS_URLRedis, usado pelo limite de taxa e pelo publicador de eventosSim
JWT_SECRETSegredo HS256 compartilhado com o IAM. Mínimo 44 caracteresSim
S3_*Endpoint, credenciais e bucket, para resolver fileIdSim, se usar fileId
PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENToff aceita webhook não verificável com aviso; on recusaNãooff
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith
PORTPorta em standaloneNão3000 (a topologia expõe 3014)

A chave mestra é lida direto de process.env no 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, com INTERNAL. Confira a presença dela no deploy; não confie na subida limpa como sinal.

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema esignature — configurações, envelopes, signatários, documentos e eventos
RedisLimite de taxa e publicação de eventos
S3 ou MinIOResolver fileId em conteúdo, via URL assinada
IAMVerificação do token; o organizationId vem do claim assinado
Instância do provedorHoje, uma instalação OpenSign alcançável pelo serviço

Limites e quotas

LimiteValorOnde
Corpo da requisição1 MBapplyCommonMiddleware — é o teto prático de fileBase64
Espera por chamada ao OpenSign30 segundosCliente HTTP do provider
Upload ao OpenSign100 MBLimite do cliente de upload, inalcançável pelo teto de 1 MB da API
title255 caracteresZod
message2.000 caracteresZod
expiresInDays1 a 365Zod
Signatáriosmínimo 1, sem teto declaradoZod
Limite de taxa global10.000 requisições por minuto por IP, quando ligadoRATE_LIMIT_GLOBAL_MAX

Eventos publicados

EventoQuando
esignature.request.createdSolicitação criada, por qualquer das duas rotas de criação
esignature.request.cancelledCancelamento concluído
esignature.request.completedWebhook completed processado
esignature.request.declinedWebhook declined processado
esignature.signer.viewedWebhook viewed processado
esignature.signer.signedWebhook 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> }.

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado, origem de arquivo ambígua, estado incompatível com a operaçãoLeia a message
401Token ausente, inválido ou expiradoRenove no IAM
403Falta a permissão exigida, ou o token não carrega organizationIdConfira o papel e a organização
404NOT_FOUNDConfiguração, solicitação ou signatário inexistente, excluído, ou de outra organizaçãoConfira o identificador. "De outra organização" e "inexistente" são a mesma resposta, de propósito
409CONFLICTNome de configuração repetido na organizaçãoEscolha outro
413Corpo acima de 1 MBUse fileId
429Limite de taxa estouradoAplique recuo exponencial
500INTERNALProvedor não implementado; chave mestra ausente; falha ao baixar do S3; erro do provedorVerifique 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/health responde a sonda de disponibilidade. É a rota usada pelo healthcheck da 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 em payload e o motivo da falha em error. É 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.

14

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 descarta

O 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"] -.-> T

Os 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ívelDefinição legal, resumidaO OpenSign atende?
I — simplesPermite identificar o signatário e anexa ou associa dados a outros dados eletrônicos do signatárioSim
II — avançadaUsa 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 posteriorNão — falta o controle exclusivo e falta a validação de identidade que o Decreto nº 10.543/2020, art. 5º, II exige
III — qualificadaUsa certificado digital nos termos do art. 10, § 1º da MP nº 2.200-2 — ou seja, ICP-BrasilNã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 legalPresunção de veracidade em relação aos signatáriosVálido entre as partes que o admitiram
Quem tem o ônus da provaQuem contesta precisa desconstituirQuem invoca precisa provar autoria e integridade
É o que este building block entrega hoje?NãoSim

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. 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 guardaOnde
Quem foi convidado a assinar — nome e e-mailsigners
A ordem exigida entre os signatáriossigners.order
Quando cada um recebeu, visualizou, assinou ou recusousigners.sentAt, viewedAt, signedAt, declinedAt
O motivo da recusa, quando o provedor o informasigners.declineReason
Todo evento recebido do provedor, com o corpo originalesignature_webhook_events.payload
O vínculo com o documento e com a operaçãosignature_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ênciaO OpenSign produz?O building block captura?
Hash SHA-256 do documento assinadoSim — calculado sobre o PDF final e persistido do lado deleNão. Nenhuma coluna do nosso schema guarda resumo criptográfico
Trilha de auditoria com endereço IP por eventoSim — por usuário, atividade, IP e horárioNão. Não há mapeamento, e a interface de provedor não tem método para buscá-la
Certificado de conclusãoSim — PDF com identificador, hash, organização, datas e, por signatário, nome, e-mail, IP e horárioNã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çaNão se aplica
Autenticação adicional do signatário por códigoExiste no OpenSignNã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çãoA assinatura simples basta?
Contrato entre partes privadas que aceitaram o meio eletrônicoEm 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ônicoSim, pelo art. 784, § 4º do CPC — desde que a integridade seja conferida pelo provedor
Emitir CCBSim — 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 MEINão — o art. 5º, § 2º, III da Lei nº 14.063 exige qualificada
Ato de transferência e registro de bem imóvelNã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édicoNão — o art. 13 da Lei nº 14.063 exige qualificada
Ato em que a autoria pode ser contestada em juízoDepende 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.


15

Limitações conhecidas

LimitaçãoImpactoSituação
Só o OpenSign está implementadoDOCUSIGN, 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ãoRoadmap — não anuncie multi-provedor como capacidade disponível
A rota de webhook não está funcionalA 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çãoBloqueante para acompanhamento automático — corrigir é prioridade
Não há rota para reconsultar o estado no provedorO provider implementa getSignatureRequest, mas nenhuma rota o expõe. Com o webhook indisponível, não há caminho pela API para atualizar o estadoRoadmap — é a mitigação natural do item anterior
O OpenSign não assina webhooksEle 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 bordaLimitação do provedor
Nenhum provedor com ICP-BrasilO 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 capturadasO 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 é ativadaO 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ívelRoadmap
O nível de assinatura não é modeladoNã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çãoRoadmap
O OpenSign não emite carimbo de tempo confiávelOs horários da trilha são do relógio do servidor, sem autoridade de carimbo de tempo (RFC 3161). Para prova de anterioridade, isso é fracoLimitação do provedor
A licença do OpenSign não é AGPL-3 puraO 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 comercialPendente de avaliação jurídica
Corpo limitado a 1 MBfileBase64 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 fileIdPor design do middleware comum; ajustável na infraestrutura
Criar configuração não valida credencialUma credencial errada, ou um providerType não implementado, é aceita e só falha depois. Sempre chame POST /:id/testPor design; o teste existe para isso
EXPIRED nunca é gravadoO estado existe no enum e é mapeado a partir do provedor, mas nenhum caminho do código o persiste. Envelope vencido continua no último estado conhecidoRoadmap
Reenvio é efeito colateral, não contratoNo OpenSign, reenviar é atualizar um campo do documento para disparar a notificação. A resposta de sucesso não garante que um e-mail saiuLimitação do provedor
from-template usa template do provedorA 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 nomePor design, mas confunde
Sem rota para listar templates do provedorO provider implementa listTemplates, mas nenhuma rota o expõe. Descobrir o templateId exige acessar o painel do provedorRoadmap
Sem rota para ler os eventos de webhookesignature_webhook_events é gravada mas não exposta. Investigar divergência exige consulta ao bancoRoadmap
Sem rotação da chave mestraTrocar ESIGNATURE_CREDENTIAL_MASTER_KEY inutiliza todas as credenciais já cifradas. Não há procedimento de recifragemRoadmap
Permissões só no papel ADMINESIGNATURE_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 provedorConfiguração pendente no IAM
Sem retenção nem expurgo automatizadoNome e e-mail de signatário ficam indefinidamente. Atender pedido de eliminação da LGPD é processo manualRoadmap
Sem lembrete configurávelO OpenSign é configurado com lembrete automático a cada 3 dias, valor fixo no código. Não há como ajustar pela APIRoadmap

16

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