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:
- Sem HMAC documentado no webhook
- 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ódigoasync 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ção | Path | Notas |
|---|---|---|
| Upload de documento | POST /accounts/{accountId}/documents | Tem prefixo /accounts/... |
| Consulta de status | GET /documents/{id} | Sem prefixo |
| Adicionar signatário | POST /documents/{id}/assignments | Sem prefixo |
| Download do PDF assinado | GET /documents/{id}/download | Sem 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)
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áriosdocument.completed— fluxo completodocument.canceled— cancelado por uma das partesdocument.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_urlem Supabase Storage assim que documento é assinado (cópia local)
- Mitigação: armazenar
- 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.mddocs/edge-functions/create-contract.md
Referências
- ADR 0006 (funil comercial)
- AGENTS.md §5.5