Pular para o conteúdo

0007 — Assinafy como Provider de Assinatura Digital

Status

Accepted · 2026-04-26 · Autor: Alexandre + Claude.

Contexto

V1 começou com ZapSign, migrou pra Assinafy. V2 nasce com apenas Assinafy — ZapSign descartado.

Razões da escolha por Assinafy:

  • API brasileira, foco mercado nacional
  • Aceita assinatura via certificado ICP-Brasil (caso necessário)
  • Customer support em português
  • Pricing competitivo

Mas Assinafy tem 2 nuances técnicas conhecidas que precisam ser tratadas com regras invioláveis:

  1. Sem HMAC documentado no webhook
  2. Path inconsistency entre endpoints REST

Decisão

1. Único provider: Assinafy. Sem ZapSign no v2.

Toda referência a “assinatura digital”, “webhook de signatura” ou “provider de assinatura” aponta para Assinafy. Sem fallback pra outro provider em F1-F2.

(Em F3+, se necessário, abrir ADR pra avaliar segundo provider; mas por enquanto Assinafy é único.)

2. Conta da plataforma + conta por consultoria (BYOK)

  • Conta plataforma (sua, Alexandre): contratos B2B com consultores parceiros (assinatura do “Termo de Adesão Plataforma”)
  • Conta por consultoria (Karla + futuros): contratos B2C com clínicas (assinatura do contrato comercial consultoria↔clínica)

Resolver getAssinafyConfig(consultancyId) decifra credenciais de consultancy_billing_accounts em runtime. Para contratos B2B platform-side, getPlatformAssinafyConfig().

3. Webhook sem HMAC → reconsulta autenticada

Edge function assinafy-webhook recebe notificação. Antes de mudar estado interno:

// Pseudo-código
async function handleAssinafyWebhook(payload: unknown) {
// 1. Parse payload com Zod passthrough() (regra inviolável §5.5.5)
const event = AssinafyWebhookSchema.parse(payload);
// 2. Idempotência por document_id
const eventId = extractDocumentId(event); // tenta múltiplos paths (object.id, document_id, etc)
await insertBillingEvent(eventId, event); // ON CONFLICT DO NOTHING
// 3. Reconsulta API AUTENTICADA pra confirmar status real
const config = await getAssinafyConfigForDocument(eventId);
const realStatus = await fetchAssinafyDocumentStatus(eventId, config);
// 4. Atualiza estado interno BASEADO em realStatus, não no payload
if (realStatus.status === 'signed') {
await markContractAsSigned(eventId, realStatus);
}
}

Razão: Assinafy não documenta HMAC; payload pode ser falsificado. Reconsulta autenticada com API key garante autenticidade.

4. Path inconsistency documentada

Documentação Assinafy é inconsistente entre endpoints:

OperaçãoPathNotas
Upload de documentoPOST /accounts/{accountId}/documentsTem prefixo /accounts/...
Consulta de statusGET /documents/{id}Sem prefixo
Adicionar signatárioPOST /documents/{id}/assignmentsSem prefixo
Download do PDF assinadoGET /documents/{id}/downloadSem prefixo

Implementação: fetchAssinafyDocumentStatus tem fallback de path — tenta primeiro com prefixo /accounts/{accountId}/, se 404, tenta sem prefixo. Resposta expõe upstream_path_used no body pra diagnóstico via curl.

5. Schema de webhook tolerante (Zod passthrough)

packages/schemas/src/webhooks/assinafy.ts
import { z } from 'zod';
const AssinafyWebhookSchema = z
.object({
event: z.string(),
object: z
.object({
id: z.string().optional(),
})
.passthrough()
.optional(),
document_id: z.string().optional(), // versão alternativa
data: z.unknown().optional(),
})
.passthrough();
export type AssinafyWebhook = z.infer<typeof AssinafyWebhookSchema>;
export function extractDocumentId(payload: AssinafyWebhook): string | null {
return payload.object?.id ?? payload.document_id ?? null;
}

Adicionar variação NUNCA quebra; remover variação aceita pode romper E2E silenciosamente (regra inviolável §5.5.5).

6. Eventos suportados

  • document.signed — documento foi assinado por todos os signatários
  • document.completed — fluxo completo
  • document.canceled — cancelado por uma das partes
  • document.refused — signatário recusou

Cada evento aciona side-effect específico (atualizar contracts.status, disparar provisioning, etc).

7. Templates de documento

Plataforma mantém template canônico de contrato (HTML que vira PDF via geração — ADR 0006 §5). Template tem placeholders preenchidos por snapshot (proposta + dados fiscais + Programa Acolhe). PDF gerado é enviado pro Assinafy via API; signatários adicionados; envelope criado.

Quando todos assinam, Assinafy notifica via webhook → reconsulta → contracts.status = 'signed' → trigger provisão.

8. Signatários

  • Contrato consultoria↔clínica (B2C):

    • Signatário 1: representante legal da clínica (clinic_owner)
    • Signatário 2 (opcional): testemunha
    • Signatário 3 (opcional): representante consultoria (alguns consultores assinam manualmente fora; outros usam Assinafy)
  • Contrato plataforma↔consultoria (B2B):

    • Signatário 1: representante legal da consultoria
    • Signatário 2: representante ClinicGestor (Alexandre, automatizado)

9. Sandbox vs produção

URLs distintas:

  • Sandbox: https://api-sandbox.assinafy.com.br/v1
  • Produção: https://api.assinafy.com.br/v1

Resolver env-aware (regra §5.5.2) detecta env e seleciona URL correta.

Consequências

Positivas

  • Único provider simplifica arquitetura (vs ZapSign + Assinafy do v1)
  • Reconsulta autenticada mitiga risco de webhook falsificado
  • Fallback de path evita quebrar quando docs Assinafy mudam
  • Schema tolerante (passthrough) evita quebrar com variações de payload

Negativas

  • Vendor lock-in com Assinafy (mudar provider exige migração de envelopes ativos)
  • Reconsulta autenticada adiciona latência ao webhook (mas é seguro)

Riscos

  • Assinafy mudar API breaking → contratos antigos ficam inconsultáveis
    • Mitigação: armazenar signed_pdf_url em Supabase Storage assim que documento é assinado (cópia local)
  • Assinafy ficar fora do ar → fluxo de provisão para
    • Mitigação: monitor + alerta; manual fallback (gerar PDF + assinatura manual via DocuSign ou similar — caso de borda)

Alternativas consideradas

1. ZapSign (legado v1)

  • Prós: já usado no v1
  • Contras: experiência v1 mostrou maior taxa de bugs; UX inferior; pricing pior em volume
  • Rejeitado.

2. DocuSign

  • Prós: padrão internacional
  • Contras: pricing alto; UX em PT-BR limitada; foco enterprise
  • Rejeitado pra MVP.

3. Auto-assinatura via certificado ICP-Brasil próprio

  • Prós: zero dependência externa
  • Contras: complexidade técnica enorme; não cobre signatário sem certificado (90% dos clientes)
  • Rejeitado.

Atualizações de documentos

  • AGENTS.md §2 (stack), §5.5.4-5 (regras de webhook)
  • docs/edge-functions/assinafy-webhook.md
  • docs/edge-functions/create-contract.md

Referências

  • ADR 0006 (funil comercial)
  • AGENTS.md §5.5