Como Integrar Assinatura Digital em Node.js / TypeScript

Integrar assinatura digital em uma aplicação Node.js / TypeScript não precisa ser complicado. Com o SDK oficial da SignDocs Brasil, você sai do zero a um documento assinado com validade jurídica em poucas linhas de código: autentica via OAuth2, cria uma sessão de assinatura, envia ao signatário, recebe o evento de conclusão por webhook e baixa o PDF assinado. Este guia mostra o caminho completo, com snippets idiomáticos em async/await.

O público deste tutorial é o desenvolvedor backend Node que precisa coletar assinaturas eletrônicas ou digitais (ICP-Brasil) dentro do próprio produto — um SaaS, um sistema de RH, uma fintech ou um marketplace. Vamos usar a Assinatura Expressa (POST /v1/signing-sessions) como caminho principal, por ser a forma mais rápida de colocar uma assinatura em produção, e comentar quando faz sentido migrar para a Transaction API de envelopes.

Se você ainda está avaliando a plataforma, vale a leitura do panorama na página da API de assinatura digital e do guia-pilar o que é uma API de assinatura digital. Já quem trabalha com Python ou apenas REST puro pode acompanhar os equivalentes em Python e cURL / REST.

Pré-requisitos

Antes de começar, garanta que você tem o ambiente pronto:

  • Node.js 18 ou superior — versões LTS recentes já incluem fetch e crypto nativos, usados nos exemplos.
  • Uma conta SignDocs com credenciais de homologação — o sandbox é gratuito e self-service (conta PJ); o acesso de produção à API é contratado como plano sob medida. O passo a passo está em como obter sua API key.
  • Credenciais de API — um client_id e um client_secret para o fluxo OAuth2 client-credentials.
  • Acesso ao ambiente de homologação (host api-hml.signdocs.com.br) para testar sem afetar dados reais.
Sandbox primeiro. Toda a integração descrita aqui deve ser construída e validada em homologação antes de ir para produção. As entidades criadas em homologação têm TTL de 7 dias e são apagadas automaticamente — perfeito para testes, mas nunca confie nelas como armazenamento durável.

1. Instalar o SDK oficial TypeScript

A SignDocs mantém SDKs oficiais para TypeScript/Node, Python, Go, Java, PHP e C#/.NET. Para Node, instale o SDK oficial TypeScript da SignDocs via npm (ou o gerenciador de sua preferência):

# npm npm install @signdocs-brasil/api # ou yarn yarn add @signdocs-brasil/api # ou pnpm pnpm add @signdocs-brasil/api

O pacote é distribuído com tipos TypeScript embutidos, então o autocompletar e a checagem de tipos funcionam imediatamente em editores como VS Code, sem instalar pacotes @types/* adicionais. Em projetos JavaScript puro o SDK funciona da mesma forma — você apenas perde a verificação estática de tipos.

Estrutura mínima do projeto

signdocs-integracao/ ├── src/ │ ├── client.ts # cliente SDK + autenticação │ ├── criar-sessao.ts # cria a Assinatura Expressa │ └── webhook.ts # receptor Express + verificação HMAC ├── .env # credenciais (NUNCA versionar) ├── package.json └── tsconfig.json

2. Configurar a autenticação OAuth2 client-credentials

A API SignDocs usa o fluxo OAuth2 client-credentials: sua aplicação troca client_id + client_secret por um token de acesso (bearer JWT) que expira em 15 minutos, assinado com ECDSA (ES256) e com chaves protegidas em KMS. Esse token é então enviado no header Authorization de cada chamada. O SDK cuida dessa troca para você, mas é importante entender o mecanismo — os detalhes completos estão no guia de autenticação OAuth2 da API.

Carregue as credenciais de variáveis de ambiente. Nunca coloque o client_secret em código versionado:

// src/client.ts import { SignDocsBrasilClient } from '@signdocs-brasil/api'; export const signdocs = new SignDocsBrasilClient({ clientId: process.env.SIGNDOCS_CLIENT_ID!, clientSecret: process.env.SIGNDOCS_CLIENT_SECRET!, // Homologação: api-hml.signdocs.com.br | Produção: api.signdocs.com.br baseUrl: process.env.SIGNDOCS_BASE_URL ?? 'https://api-hml.signdocs.com.br', });

Internamente, o cliente realiza o fluxo client-credentials, armazena o token em memória e o renova automaticamente quando expira. Caso prefira controlar o fluxo manualmente (por exemplo, em REST puro sem o SDK), a troca de token é uma chamada direta:

// Troca manual de token OAuth2 (opcional, sem SDK) async function obterToken(): Promise<string> { const res = await fetch('https://api-hml.signdocs.com.br/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.SIGNDOCS_CLIENT_ID!, client_secret: process.env.SIGNDOCS_CLIENT_SECRET!, }), }); if (!res.ok) { throw new Error(`Falha na autenticação: ${res.status}`); } const { access_token } = await res.json(); return access_token; }
mTLS para clientes regulados. Para integrações enterprise em setores regulados (BACEN, Open Finance), a SignDocs oferece autenticação por mTLS (mutual TLS) além do OAuth2. Veja a documentação de segurança mTLS para configurar certificados de cliente no agente HTTPS do Node.

3. Criar uma sessão de Assinatura Expressa

Com o cliente autenticado, criar uma sessão de assinatura é uma única chamada a POST /v1/signing-sessions. Você define o documento, o signatário e o perfil de assinatura (profile), que determina o nível de exigência probatória.

Anatomia da requisição

A tabela abaixo resume os campos principais do corpo da requisição:

Campo Descrição
document O PDF a ser assinado, enviado como base64 no campo content, com filename opcional (até 10 MB).
signer Dados do signatário: nome, e-mail, o userExternalId (o ID dele no seu sistema) e CPF ou CNPJ (sem pontuação) para vinculação de identidade.
policy.profile Perfil de assinatura. Use DIGITAL_CERTIFICATE para exigir certificado ICP-Brasil; CLICK_ONLY, CLICK_PLUS_OTP, BIOMETRIC e BIOMETRIC_PLUS_OTP cobrem os níveis eletrônicos.
returnUrl Para onde o signatário é redirecionado após concluir (opcional). Com owner informado, a SignDocs envia o convite por e-mail; sem ele, você entrega o link.
Webhooks Registrados uma única vez por tenant via POST /v1/webhooks (não por sessão) — é por lá que os eventos do ciclo de vida chegam.
Atenção ao profile. O valor DIGITAL_SIGN_A1 é um tipo de passo (step.type) que aparece na resposta, não um valor válido de policy.profile. Para exigir certificado digital ICP-Brasil, use DIGITAL_CERTIFICATE — via API, o certificado é A1 (em arquivo); para titulares de token A3, o aplicativo SignDocs oferece o assinador desktop. Enviar DIGITAL_SIGN_A1 como profile resulta em erro 400.

Código: criar a sessão e adicionar o signatário

// src/criar-sessao.ts import { readFile } from 'node:fs/promises'; import { signdocs } from './client'; export async function criarSessaoExpressa() { // 1. Carregar o PDF e codificar em base64 const pdf = await readFile('./contrato.pdf'); // 2. Criar a sessão de Assinatura Expressa const sessao = await signdocs.signingSessions.create({ purpose: 'DOCUMENT_SIGNATURE', policy: { // CLICK_PLUS_OTP = aceite + código por e-mail/SMS. // Use DIGITAL_CERTIFICATE para exigir certificado ICP-Brasil (A1). profile: 'CLICK_PLUS_OTP', }, signer: { name: 'Maria Silva', email: 'maria@empresa.com.br', userExternalId: 'usr_12345', // ID no SEU sistema cpf: '12345678901', // 11 dígitos, sem pontuação }, document: { content: pdf.toString('base64'), filename: 'Contrato de Prestação de Serviços.pdf', }, returnUrl: 'https://app.suaempresa.com.br/assinatura-concluida', }); console.log('Sessão criada:', sessao.sessionId); // Link hospedado final = url + "?cs=" + clientSecret console.log('URL de assinatura:', `${sessao.url}?cs=${sessao.clientSecret}`); return sessao; }

A resposta inclui o sessionId, a url e o clientSecret. Para o checkout hospedado, monte o link final (url + "?cs=" + clientSecret) e redirecione o usuário ou envie por e-mail. Para a assinatura embarcada (embedded), entregue o clientSecret ao seu front-end e abra o checkout como popup com o SDK @signdocs-brasil/js (SignDocsBrasil.init() + sd.checkout({ clientSecret })) — o que mantém o usuário no seu produto.

Sender não deve assinar como signatário. Se o e-mail do remetente for igual ao do signatário, você pode acabar abrindo o link e assinando no lugar dele. Em add-ons e portais, condicione a abertura automática a signerEmail === senderEmail; caso contrário, apenas entregue o link.

Embedded vs. checkout hospedado

A escolha do modo de entrega depende da experiência desejada. Para aprofundar, veja a página da Assinatura Expressa.

Modo Quando usar
Checkout hospedado Quer o caminho mais rápido. A SignDocs hospeda a página de assinatura; você só compartilha a URL. Zero código de front-end.
Embedded (SDK / popup) Quer manter o usuário dentro do seu produto, com sua marca. Exige abrir o checkout no front-end via SDK @signdocs-brasil/js com o clientSecret.

4. Quando usar a Transaction API (envelopes)

A Assinatura Expressa resolve a maioria dos casos de um signatário. Quando o fluxo envolve múltiplos signatários no mesmo documento, com ordem de assinatura ou um ciclo de vida mais elaborado, a API de envelopes é a escolha certa. Ela está exposta no mesmo SDK:

// Criar um envelope com dois signatários em ordem const envelope = await signdocs.envelopes.create({ signingMode: 'SEQUENTIAL', // ou 'PARALLEL' totalSigners: 2, document: { content: pdf.toString('base64'), filename: 'Contrato.pdf', }, }); // Uma sessão por signatário — signerIndex define a posição na fila, // e cada um pode ter sua própria política await signdocs.envelopes.addSession(envelope.envelopeId, { signerIndex: 1, signer: { name: 'Maria Silva', email: 'maria@empresa.com.br', userExternalId: 'usr_maria', cpf: '12345678901' }, policy: { profile: 'DIGITAL_CERTIFICATE' }, }); await signdocs.envelopes.addSession(envelope.envelopeId, { signerIndex: 2, signer: { name: 'João Souza', email: 'joao@empresa.com.br', userExternalId: 'usr_joao', cpf: '98765432100' }, policy: { profile: 'CLICK_PLUS_OTP' }, });

Os fundamentos do ciclo de vida transacional — estados, transições e finalização — estão detalhados no guia de fluxo transacional da API. Para regras de ordenação entre signatários, consulte ordem de assinatura com múltiplos signatários.

5. Receber o webhook e verificar a assinatura HMAC-SHA256

Em vez de ficar consultando o status repetidamente (polling), o caminho recomendado é receber webhooks: a SignDocs envia um HTTP POST ao seu endpoint a cada evento — SIGNING_SESSION.COMPLETED, TRANSACTION.COMPLETED, STEP.FAILED, entre outros. Cada requisição traz uma assinatura HMAC-SHA256 em um header, que você precisa verificar para garantir que o evento veio mesmo da SignDocs. A arquitetura completa de eventos está no guia de webhooks e eventos da API.

Receptor Express com verificação HMAC

O ponto crítico é preservar o corpo bruto (raw body) da requisição: o HMAC é calculado sobre os bytes exatos recebidos, então qualquer reserialização do JSON quebraria a verificação.

// src/webhook.ts import express from 'express'; import { verifyWebhookSignature } from '@signdocs-brasil/api'; const app = express(); const WEBHOOK_SECRET = process.env.SIGNDOCS_WEBHOOK_SECRET!; // Preservar o corpo bruto para o cálculo do HMAC app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf.toString('utf8'); }, })); app.post('/webhooks/signdocs', (req, res) => { const signature = req.headers['x-signdocs-signature'] as string; const timestamp = req.headers['x-signdocs-timestamp'] as string; const rawBody = (req as any).rawBody as string; // O helper do SDK valida o HMAC-SHA256 (timestamp + "." + corpo bruto), // com comparação timing-safe e janela anti-replay de 5 minutos if (!verifyWebhookSignature(rawBody, signature, timestamp, WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Assinatura inválida' }); } const evento = req.body; // Responda 200 rápido; processe de forma assíncrona res.status(200).json({ status: 'received' }); // Despachar conforme o tipo de evento (idempotente!) processarEvento(evento).catch(console.error); }); app.listen(3000, () => console.log('Webhook na porta 3000'));

Despachar eventos de forma idempotente

Webhooks têm semântica de entrega pelo menos uma vez: o mesmo evento pode chegar duplicado após um retry. Use o id do evento para deduplicar antes de processar. O payload real é { id, eventType, tenantId, transactionId, timestamp, data }:

const processados = new Set<string>(); // em produção, use Redis/DB async function processarEvento(evento: any) { if (processados.has(evento.id)) return; // já tratado processados.add(evento.id); switch (evento.eventType) { case 'SIGNING_SESSION.COMPLETED': console.log('Signatário concluiu a sessão:', evento.transactionId); break; case 'TRANSACTION.COMPLETED': await baixarPdfAssinado(evento.transactionId); break; case 'SIGNING_SESSION.CANCELLED': console.warn('Sessão cancelada:', evento.transactionId); break; } }
Dica de produção. Em ambiente serverless ou com múltiplas réplicas, a deduplicação com um Set em memória não basta — use um armazenamento compartilhado (Redis com SET NX, ou uma tabela com chave única no id do evento). Responda 200 OK em segundos e empurre o trabalho pesado para uma fila.

6. Baixar o PDF assinado e o pacote de evidências

Quando a transação é concluída (TRANSACTION.COMPLETED), você recupera os artefatos finais: o PDF assinado no padrão PAdES e o pacote de evidências .p7m — um container PKCS#7/CMS que vincula o hash SHA-256 do documento ao carimbo de hora do servidor (timestamps ISO-8601 registrados pelos servidores da SignDocs) e aos dados de autenticação que comprovam quem assinou, quando e como.

// Baixar o PDF assinado e o evidence pack .p7m import { writeFile } from 'node:fs/promises'; import { signdocs } from './client'; async function baixarPdfAssinado(transactionId: string) { // 1. URLs temporárias do documento (o PDF carimbado vem em signedUrl) const dl = await signdocs.documents.download(transactionId); const pdf = Buffer.from(await (await fetch(dl.signedUrl!)).arrayBuffer()); await writeFile(`./assinados/${transactionId}.pdf`, pdf); // 2. Evidence pack (.p7m): metadados + download via verificação const evidencia = await signdocs.evidence.get(transactionId); const downloads = await signdocs.verification.downloads(evidencia.evidenceId); const p7mUrl = downloads.downloads.evidencePack?.url; if (p7mUrl) { const p7m = Buffer.from(await (await fetch(p7mUrl)).arrayBuffer()); await writeFile(`./assinados/${transactionId}.p7m`, p7m); } console.log('Documentos finais salvos para', transactionId); }

O PDF assinado preserva validade jurídica no Brasil conforme a MP 2.200-2/2001 (ICP-Brasil) e a conformidade com a LGPD. Qualquer parte pode conferir a integridade do documento no verificador público da SignDocs. Para entender a estrutura probatória do .p7m, veja o guia de evidence pack e prova jurídica.

Boas práticas para produção

  • Segredos fora do código: carregue client_secret e webhook_secret de variáveis de ambiente ou de um cofre (AWS Secrets Manager, Vault). Reaproveite o token OAuth2 em memória enquanto válido.
  • Idempotência sempre: trate cada webhook como possivelmente duplicado. A chave de deduplicação é o id do evento.
  • Comparação timing-safe: valide o HMAC com crypto.timingSafeEqual, nunca com === direto.
  • Responda rápido: retorne 200 OK em segundos e processe o evento em uma fila assíncrona; webhooks têm timeout curto.
  • Homologação antes de produção: teste tudo em api-hml.signdocs.com.br; lembre-se do TTL de 7 dias das entidades de sandbox.
  • Tratamento de erros: a API retorna códigos HTTP semânticos. Implemente retry com backoff em erros 5xx e trate 4xx como problema de requisição.
SignDocs: SDK Node pronto para produção. O SDK oficial TypeScript da SignDocs abstrai OAuth2, retries e tipagem completa da API, e funciona de uma aplicação Express tradicional a uma função AWS Lambda. Comece no sandbox de homologação gratuito e migre para produção com as mesmas linhas de código — o acesso à API é um plano sob medida. Fale com nosso time para receber as credenciais.

Perguntas Frequentes

Preciso usar TypeScript para integrar a API de assinatura em Node.js?

Não. O SDK oficial da SignDocs é escrito em TypeScript, mas funciona perfeitamente em projetos JavaScript puro (CommonJS ou ES Modules). O TypeScript apenas oferece autocompletar e checagem de tipos no editor, o que reduz erros de integração. Se preferir não usar o SDK, todas as operações também estão disponíveis via REST puro com fetch ou axios, já que a API é agnóstica de linguagem.

Como armazeno o token OAuth2 com segurança em uma aplicação Node.js?

Nunca coloque o client_secret diretamente no código-fonte nem no repositório. Carregue-o de variáveis de ambiente (process.env) ou de um gerenciador de segredos como AWS Secrets Manager, HashiCorp Vault ou Doppler. O token de acesso (bearer) emitido pelo fluxo client-credentials é de curta duração; mantenha-o apenas em memória, reaproveite-o enquanto for válido e solicite um novo somente quando expirar.

Qual a diferença entre a Assinatura Expressa e a Transaction API no SDK Node?

A Assinatura Expressa (POST /v1/signing-sessions) cria em uma única chamada uma sessão pronta para assinar, retornando a URL de checkout hospedado e o clientSecret para o checkout embutido via SDK JavaScript. É ideal para fluxos rápidos de um signatário. A API de envelopes suporta múltiplos signatários no mesmo documento (até 100), com ordem sequencial ou paralela e o ciclo de vida completo. Ambas estão expostas no mesmo SDK; escolha conforme a complexidade do fluxo.

Como recebo notificações de que o documento foi assinado em Node.js?

Configure um endpoint HTTPS na sua aplicação e registre a URL como webhook. A SignDocs envia um HTTP POST a cada evento (por exemplo SIGNING_SESSION.COMPLETED, TRANSACTION.COMPLETED) com uma assinatura HMAC-SHA256 no header X-SignDocs-Signature. No receptor Express, preserve o corpo bruto da requisição e valide com o helper verifyWebhookSignature do SDK @signdocs-brasil/api (que faz a comparação timing-safe e a checagem anti-replay do timestamp) antes de processar o evento. Responda 200 OK rapidamente e processe de forma assíncrona.

O SDK Node funciona em AWS Lambda e outras arquiteturas serverless?

Sim. O SDK não depende de estado de servidor e funciona em AWS Lambda, Google Cloud Functions, Vercel e Cloudflare Workers compatíveis com Node. Recomenda-se inicializar o cliente fora do handler para reaproveitá-lo entre invocações no mesmo contexto de execução, e cachear o token OAuth2 em memória para evitar pedir um novo token a cada chamada. Considere o cold start ao configurar timeouts de webhook.

Posso testar a integração em homologação antes de ir para produção?

Sim. A SignDocs oferece um ambiente de homologação (sandbox) com host api-hml.signdocs.com.br, usado com credenciais separadas das de produção. Basta apontar a baseUrl do SDK para o host de homologação. Lembre-se de que as entidades criadas em homologação têm TTL de 7 dias, ou seja, expiram automaticamente após esse período — o ambiente é destinado a testes, não a dados persistentes.

Como baixo o PDF assinado com a trilha de evidências em Node.js?

Após o evento TRANSACTION.COMPLETED, use o SDK para recuperar os artefatos finais: client.documents.download(transactionId) devolve URLs temporárias (o PDF carimbado vem em signedUrl), e o pacote de evidências .p7m (container PKCS#7/CMS que vincula o hash SHA-256 do documento ao carimbo de hora do servidor e aos dados de autenticação) é obtido via client.evidence.get + client.verification.downloads. Baixe os arquivos dessas URLs e grave em disco, em um bucket S3 ou envie às partes. O PDF preserva a validade jurídica conforme a MP 2.200-2/2001 e a integridade pode ser conferida no verificador público.

Coloque assinatura digital no seu app Node.js hoje

Do npm install @signdocs-brasil/api ao PDF assinado: o SDK oficial TypeScript da SignDocs cuida de OAuth2, Assinatura Expressa, webhooks HMAC e download de evidências. O sandbox de homologação é gratuito; o acesso à API é um plano sob medida com o time comercial.

Fale com o time comercial Conheça a plataforma grátis