Assinatura Digital em SaaS Multi-tenant: Isolar Clientes, Rotear Webhooks e Repassar Custos

Quando um SaaS integra assinatura digital, o problema raramente é a chamada de API. O problema é o que vem depois: dezenas ou centenas de clientes finais criando documentos pela mesma integração, um único webhook recebendo tudo, uma única cota sendo consumida e, no fim do mês, a pergunta inevitável do financeiro: quanto cada cliente gastou? Este guia mostra como estruturar a integração com a API da SignDocs Brasil em um produto multi-tenant, usando o que a API oferece de fato e sendo honesto sobre o que ela não oferece.

O modelo é simples de enunciar: uma credencial de API = um tenant do ponto de vista da SignDocs = uma cota. Todo o resto (quem é o cliente final, a que contrato aquele documento pertence, para qual fila o evento deve ir) é responsabilidade do seu sistema, e a API dá dois mecanismos para carregar essa informação de ponta a ponta: metadata e signer.userExternalId. O guia de API de assinatura digital cobre a base; aqui o foco é a camada multi-tenant que você constrói em cima dela.

O que a API enxerga e o que só o seu sistema sabe

Antes de desenhar tabelas e filas, vale fixar a divisão de responsabilidades. A SignDocs conhece a sua credencial, as transações criadas com ela, os signatários de cada transação e os pares chave-valor que você mandou em metadata. Ela não conhece seus clientes: não há cadastro de "sub-contas", não há credencial filha por cliente final, não há filtro de webhook por cliente. Isso é uma escolha de superfície mínima, e para um SaaS costuma ser a arquitetura certa: o mapeamento cliente → documento fica onde já está a sua regra de negócio, no seu banco.

Responsabilidade SignDocs Seu SaaS
Autenticação da integração Uma credencial OAuth2 (client_credentials) por tenant contratante Guardar o segredo no backend; nunca no navegador ou no app do cliente
Identidade do cliente final Só o que vier em metadata e userExternalId Tabela de tenants, contratos e usuários; é a fonte da verdade
Cota e consumo Um pool por credencial, contado por documento Contabilizar por tenant para repassar ou limitar
Webhooks Configuração por credencial: url, events, secret Um endpoint receptor que roteia internamente por tenant
Evidências e documento assinado Trilha de auditoria, pacote .p7m, verificador público Baixar e arquivar no espaço do tenant certo, se o produto exigir

Modelando o tenant em metadata e userExternalId

O campo metadata aceita um objeto de strings (Record<string, string>) tanto em POST /v1/signing-sessions quanto em POST /v1/envelopes, e volta intacto em GET /v1/transactions/{id}. É o lugar para os identificadores que o seu roteador vai precisar depois:

  • tenant_id: o cliente do seu SaaS (a empresa que paga a você).
  • customer_id: quando o seu cliente tem clientes dele (uma imobiliária e seus inquilinos, uma escola e seus responsáveis), o identificador do nível seguinte.
  • contract_ref: a chave do registro que originou o documento no seu domínio (o contrato, o pedido, a admissão).
  • env ou app_version, se ajudar a depurar: são strings livres.

signer.userExternalId é um identificador opcional do signatário dentro da sua credencial. Use o ID que aquela pessoa tem no seu sistema (ou um hash dele). Isso permite, do lado do seu banco, ligar todas as sessões de um mesmo usuário sem depender do e-mail, que muda.

Regra prática para o conteúdo de metadata: identificadores, não dados. Coloque IDs opacos (tenant_id=t_8a1f), nunca razão social, CPF, telefone ou qualquer campo que você não gostaria de ver em um log de integração. A SignDocs guarda o que você manda; o mínimo necessário é a melhor política, e ela também simplifica a sua própria análise de LGPD.

// Criação de sessão carregando o contexto do tenant POST /v1/signing-sessions Content-Type: application/json Authorization: Bearer <access_token> X-Idempotency-Key: t_8a1f:contract:CT-2026-0917:v1 { "purpose": "DOCUMENT_SIGNATURE", "policy": { "profile": "CLICK_PLUS_OTP" }, "signer": { "name": "Ana Souza", "email": "ana@exemplo.com.br", "cpf": "12345678909", "userExternalId": "usr_5c02" }, "document": { "content": "<base64>", "filename": "contrato-CT-2026-0917.pdf" }, "returnUrl": "https://app.seusaas.com.br/t/t_8a1f/contratos/CT-2026-0917", "metadata": { "tenant_id": "t_8a1f", "customer_id": "c_113", "contract_ref": "CT-2026-0917" } }

Repare na chave de idempotência: ela é prefixada pelo tenant. A API deduplica por X-Idempotency-Key durante 24 horas (mesmo corpo devolve a mesma resposta; corpo diferente com a mesma chave devolve 409), e sem o prefixo dois tenants poderiam colidir em referências como contract-1. O guia de rate limits, paginação e idempotência detalha a semântica.

Envelopes: quem é o owner em um SaaS

Em POST /v1/envelopes o campo owner (name e email) identifica quem está enviando o documento para assinatura, e esse nome aparece para os signatários. Em um SaaS, o owner é o seu cliente, não a sua empresa: o inquilino deve ver "Imobiliária Horizonte" como remetente, não o nome da sua plataforma. Preencha com os dados do tenant e mantenha metadata com o tenant_id para o roteamento. Os campos de aparência do envelope permitem alguma personalização visual da página hospedada; trate isso como ajuste, não como white-label completo.

Lembre também que cada envelope carrega um documento e uma janela de assinatura (expiresInMinutes, padrão 72 horas, entre 5 minutos e 7 dias). Se o seu produto permite que o cliente configure prazos, mapeie esse valor para o intervalo permitido em vez de deixar o usuário digitar qualquer número. O guia de ordem de assinatura com múltiplos signatários cobre PARALLEL versus SEQUENTIAL.

Um webhook, muitos tenants: o roteador

A configuração de webhook é por credencial (POST /v1/webhooks com url, events e secret; listar com GET, remover com DELETE /v1/webhooks/{id}, testar com POST /v1/webhooks/{id}/test). Não existe filtro por metadata nem uma URL por cliente final. A consequência é que você precisa de um receptor único que faça três coisas, nesta ordem: validar a assinatura HMAC, responder 2xx imediatamente e resolver o tenant para encaminhar o evento à fila certa.

O payload traz eventType, um id de evento, timestamp e um objeto data com transactionId. Para descobrir o tenant, consulte a transação: GET /v1/transactions/{id} devolve o metadata que você enviou na criação. Muitos times preferem, em vez disso, gravar transactionId → tenant_id no próprio banco no momento da criação e resolver localmente, evitando uma chamada extra por evento. As duas abordagens funcionam; a segunda é mais barata e mais resistente a indisponibilidade da rede.

// receiver.ts (Node 18+, Express) — recebe, valida, roteia por tenant import express from 'express'; import { createHmac, timingSafeEqual } from 'node:crypto'; const app = express(); const SECRET = process.env.SIGNDOCS_WEBHOOK_SECRET!; function verify(raw: Buffer, sig: string, ts: string): boolean { const ageSec = Math.abs(Date.now() / 1000 - Number(ts)); if (!Number.isFinite(ageSec) || ageSec > 300) return false; const expected = createHmac('sha256', SECRET) .update(`${ts}.${raw.toString('utf8')}`).digest('hex'); const a = Buffer.from(expected), b = Buffer.from(sig || ''); return a.length === b.length && timingSafeEqual(a, b); } app.post('/webhooks/signdocs', express.raw({ type: '*/*' }), async (req, res) => { const sig = req.header('X-SignDocs-Signature') ?? ''; const ts = req.header('X-SignDocs-Timestamp') ?? ''; if (!verify(req.body, sig, ts)) return res.status(401).end(); const event = JSON.parse(req.body.toString('utf8')); // 1. Deduplicar pelo id do evento (a entrega pode se repetir) if (await db.events.exists(event.id)) return res.status(200).end(); await db.events.insert({ id: event.id, receivedAt: new Date() }); // 2. Resolver o tenant: mapa local gravado na criação, com fallback na API const txId = event.data.transactionId; let tenantId = await db.txMap.tenantOf(txId); if (!tenantId) { const tx = await signdocs.get(`/v1/transactions/${txId}`); tenantId = tx.metadata?.tenant_id; } if (!tenantId) { log.warn('evento sem tenant', event.id); return res.status(200).end(); } // 3. Responder já e processar depois, na fila do tenant await queue.publish(`tenant.${tenantId}.signing`, event); res.status(200).end(); });

Três detalhes que evitam incidentes. Primeiro, o corpo precisa ser lido bruto para o HMAC bater: qualquer middleware que faça parse de JSON antes da verificação quebra a assinatura. Segundo, a entrega tem timeout de 15 segundos e é repetida até três vezes em caso de erro 5xx ou falha de rede; depois disso a tentativa fica registrada como falha e não há reentrega indefinida. Por isso a resposta 2xx deve sair antes de qualquer processamento pesado. Terceiro, um evento que chegou sem tenant resolvível deve ser aceito (200) e logado, não rejeitado: rejeitar só gera novas tentativas do mesmo evento. A referência completa de eventos está em webhooks e eventos da API.

Reconciliação: o que fazer quando um evento se perde

Webhooks são o caminho rápido, não a única fonte da verdade. Mantenha um job de reconciliação por tenant que, para toda transação ainda marcada como pendente no seu banco há mais tempo que a janela de assinatura, consulte GET /v1/transactions/{id} e corrija o estado. Com isso, uma falha do seu receptor durante um deploy vira um atraso de minutos, não um contrato "pendente" para sempre na tela do cliente.

Contando documentos por tenant para repassar custos

A cobrança da API é por documento, não por signatário: um envelope com cinco assinantes consome um documento. Isso simplifica o repasse. A contagem que importa para você é "quantos documentos cada tenant criou no período", e a forma mais confiável de obtê-la é registrar o consumo no seu banco no momento da criação, quando você já sabe o tenant e recebeu o transactionId.

Momento O que gravar Para quê
Criação da sessão ou envelope tenant_id, transactionId, data, tipo de perfil Consumo bruto por tenant; roteamento de webhooks
SIGNING_SESSION.COMPLETED / ENVELOPE.ALL_SIGNED Estado final, evidenceId Relatório de conclusão; link do verificador para o cliente
Cancelamento ou expiração Estado final e motivo Decidir a sua política: um documento enviado e não assinado ainda contou como consumido

Dois cuidados. O primeiro é que uma cota é uma só por credencial: se um tenant disparar 3.000 documentos em uma manhã, ele consome o pool de todos. Se o seu produto tem clientes de tamanhos muito diferentes, aplique limites por tenant no seu lado antes de chamar a API; a resposta 429 da SignDocs, quando vier, traz os cabeçalhos RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset, mas nesse ponto o limite já é global. O segundo é contratual: o volume de um SaaS é a soma dos seus clientes, então o plano é dimensionado sob medida com o time comercial, e vale entrar nessa conversa com a projeção de documentos por mês por tenant. O artigo quanto custa uma API de assinatura ajuda a montar essa conta.

Isolamento de dados: o que fica onde

Em um SaaS multi-tenant a pergunta "os dados do cliente A podem vazar para o cliente B?" tem de ser respondida por construção. Na integração com a SignDocs, o isolamento vem de três decisões:

  1. Toda consulta à API passa pelo seu backend, que verifica se o usuário autenticado pertence ao tenant dono daquele transactionId antes de chamar GET /v1/transactions/{id}, /download ou /evidence. O token OAuth2 é seu, não do tenant; a autorização por tenant é sua.
  2. O link de assinatura é entregue só ao signatário, pelo canal que você já autenticou (e-mail do cadastro, notificação no app do cliente). No perfil CLICK_ONLY o link é a própria autenticação do ato, então ele não pode aparecer em listagens acessíveis a outros usuários do tenant, e muito menos a outros tenants.
  3. Arquivamento no espaço do tenant: se o seu produto guarda o PDF assinado e o pacote de evidências, baixe-os pelos endpoints de download e armazene sob a chave do tenant, com as mesmas políticas de acesso do resto do produto. O que fica na SignDocs segue a Tabela de Retenção pública; o que fica com você segue a sua.

Ambiente de homologação por tenant de teste

O sandbox (api-hml.signdocs.com.br) é gratuito, não exige cartão, simula a biometria por padrão e retém dados por 7 dias. Para um SaaS, a prática que funciona é ter um tenant de teste real no seu próprio produto apontado para credenciais de homologação, e rodar por ele o mesmo caminho que um cliente percorre: criar, receber webhook, rotear, reconciliar, contabilizar. A retenção curta é uma vantagem aqui: o que não foi promovido a produção some sozinho. O guia de ambiente de homologação descreve as diferenças de comportamento.

Checklist de arquitetura multi-tenant

  • Credencial guardada só no backend; nenhum segredo em app ou navegador.
  • metadata com tenant_id (e customer_id, contract_ref) em toda criação; só identificadores opacos.
  • signer.userExternalId com o ID interno do usuário.
  • X-Idempotency-Key prefixada pelo tenant.
  • owner do envelope = o cliente do SaaS, não a sua empresa.
  • Receptor de webhook: corpo bruto, HMAC com tolerância de 300 s, dedupe por id, 2xx imediato, roteamento por tenant.
  • Mapa local transactionId → tenant_id gravado na criação; fallback via GET /v1/transactions/{id}.
  • Job de reconciliação por tenant para transações pendentes além da janela.
  • Consumo contado por documento na criação; limites por tenant aplicados antes de chamar a API.
  • Autorização por tenant em todo download de documento e evidência.

Perguntas Frequentes

Posso criar uma credencial de API para cada cliente do meu SaaS?

Não. A credencial OAuth2 identifica você, o integrador, e há um pool de cota por credencial; não existem sub-credenciais por cliente final. O padrão para SaaS é uma credencial guardada no seu backend e o cliente final identificado em metadata (por exemplo tenant_id) e em signer.userExternalId. O mapeamento entre transações e clientes fica no seu banco, que já é a fonte da verdade do seu produto, e toda chamada de leitura ou download passa pela sua autorização por tenant.

Como faço cada cliente receber só os webhooks dele?

A configuração de webhook é por credencial, com url, events e secret, e não há filtro por metadata nem uma URL por cliente final. A solução é um receptor único que valida a assinatura HMAC, responde 2xx imediatamente e roteia o evento internamente: resolve o tenant pelo mapa local transactionId → tenant_id gravado na criação (ou, como fallback, pelo metadata devolvido em GET /v1/transactions/{id}) e publica o evento na fila daquele tenant. Deduplique pelo id do evento antes de processar.

O que colocar em metadata e o que evitar?

Coloque identificadores opacos que o seu roteador e o seu financeiro vão precisar: tenant_id, customer_id, contract_ref, talvez env ou app_version. Evite qualquer dado pessoal ou comercial legível, como razão social, CPF, telefone ou valores do contrato. A SignDocs guarda o que você envia e devolve intacto em GET /v1/transactions/{id}; quanto menos houver ali, mais simples fica o seu próprio mapeamento de tratamento de dados e menor o impacto de um log exposto.

Como repasso o custo da assinatura para cada cliente?

A cobrança é por documento, não por signatário: um envelope com vários assinantes consome um documento. Grave o consumo no seu banco no momento da criação da sessão ou do envelope, quando você já conhece o tenant e recebeu o transactionId, e feche o período a partir dessa tabela. Como a cota é única por credencial, aplique limites por tenant no seu lado antes de chamar a API; o 429 da SignDocs traz cabeçalhos RateLimit, mas nesse ponto o limite já é global. O plano de API é dimensionado sob medida com o time comercial a partir do volume somado dos seus clientes.

Quem deve aparecer como remetente do envelope: minha empresa ou meu cliente?

O seu cliente. O campo owner (name e email) de POST /v1/envelopes identifica quem envia o documento e é o nome que os signatários veem. Em um SaaS, o inquilino, o aluno ou o colaborador deve reconhecer a empresa com quem se relaciona, não a sua plataforma. Preencha owner com os dados do tenant e mantenha o tenant_id em metadata para o roteamento. Os campos de aparência do envelope permitem algum ajuste visual da página hospedada; trate-os como personalização, não como white-label completo.

E se meu receptor de webhook ficar fora do ar durante um deploy?

A entrega tem timeout de 15 segundos e é repetida até três vezes em caso de erro 5xx ou falha de rede; depois disso a tentativa fica registrada como falha e não há reentrega indefinida. Por isso o webhook não deve ser a única fonte da verdade. Mantenha um job de reconciliação por tenant que consulte GET /v1/transactions/{id} para toda transação ainda pendente no seu banco além da janela de assinatura e corrija o estado. Uma queda do receptor vira um atraso de minutos, não um contrato pendente para sempre.

Teste a arquitetura multi-tenant no sandbox

Credenciais de homologação gratuitas, sem cartão, com biometria simulada e retenção de 7 dias: rode o caminho completo de um tenant de teste antes de dimensionar o plano sob medida com o time comercial.

Criar credenciais de homologação Fale com o time comercial