Pular para o conteúdo
AssinAPI

Integração · Lovable

Assinatura eletrônica em apps Lovable

Apps Lovable têm frontend React e backend Supabase. A chamada à AssinAPI fica em uma Edge Function — o navegador nunca vê a chave.

curl -X POST /v1/signature-requests

201 Created · 312 ms

"status": "SENT", "signingUrl": "…/s/9xq4m"

webhook signer.signed → 200 OK

Antes de tudo: a chave fica no servidor

Onde fica a chave: Guarde ASSINAPI_SECRET_KEY em Supabase → Edge Functions → Secrets. Nunca em variáveis VITE_* (elas vão para o navegador).

Como funciona

  1. 1

    O usuário clica em “Enviar para assinatura” no app (React).

  2. 2

    O app chama supabase.functions.invoke("assinapi-send").

  3. 3

    A Edge Function lê ASSINAPI_SECRET_KEY e chama POST /v1/signature-requests.

  4. 4

    O signatário assina pelo link (na avançada: CPF + código pelo WhatsApp nos planos pagos ou por e-mail no Free).

  5. 5

    A AssinAPI envia o webhook envelope.completed para outra Edge Function (verify_jwt = false, valida HMAC) que atualiza a tabela do contrato.

Prompt e código prontos

Prompt para colar no Lovable

markdown
Implemente a integração com a AssinAPI neste app Lovable. Toda chamada à AssinAPI deve acontecer em uma Supabase Edge Function (backend). Guarde a chave em Supabase Secrets com o nome ASSINAPI_SECRET_KEY. O frontend React chama a Edge Function via supabase.functions.invoke — nunca a AssinAPI diretamente.

## Objetivo
Implementar: criação de envelope; upload de PDF; cadastro de signatário (nome, e-mail, CPF); envio para assinatura e armazenamento dos signingLinks; endpoint de webhook para envelope.completed (com validação HMAC); download do documento assinado e do certificado de evidências.

## Configuração
- Base URL: https://api.assinapi.com.br/v1
- Autenticação: header `Authorization: Bearer ${ASSINAPI_SECRET_KEY}`
- Leia a chave SOMENTE da variável de ambiente ASSINAPI_SECRET_KEY no servidor.
- Leia o segredo do webhook de ASSINAPI_WEBHOOK_SECRET.
- Chaves ask_test_… = Sandbox (OTP de teste 123456, CPF de teste 529.982.247-25). Chaves ask_live_… = Produção.

## Regras de segurança (obrigatórias)
- NUNCA exponha ASSINAPI_SECRET_KEY no frontend, no navegador, em app mobile, em logs ou no repositório.
- Crie uma camada server-side chamada `assinapi.service` que concentra todas as chamadas à AssinAPI.
- O frontend conversa apenas com o seu backend.
- Não registre CPF, telefone, códigos OTP ou links de assinatura em logs.

## Endpoints
- POST https://api.assinapi.com.br/v1/envelopes
  body: { "title": string, "message"?: string, "signatureLevel"?: "SIMPLE"|"ADVANCED", "expirationDays"?: number, "externalId"?: string }
  201 → { "id": "env_…", "status": "DRAFT", … }
- POST https://api.assinapi.com.br/v1/envelopes/{envelopeId}/documents
  multipart/form-data com o campo "file" (PDF)  — ou JSON { "contentBase64": string, "filename": string }
  201 → envelope com documents[].sha256
- POST https://api.assinapi.com.br/v1/envelopes/{envelopeId}/signers
  body: { "name": string, "email": string, "cpf": string, "birthDate"?: "AAAA-MM-DD", "phone"?: "+5511999999999" (envie sempre que tiver: usado no WhatsApp, planos pagos), "authenticationMethod"?: "EMAIL_LINK"|"EMAIL_OTP"|"WHATSAPP_LINK"|"WHATSAPP_OTP" (WHATSAPP_* só nos planos pagos; no Free → 402 WHATSAPP_REQUIRES_PAID_PLAN. Omitido = padrão do projeto: simples → só link por e-mail; avançada → código pelo WhatsApp nos planos pagos, por e-mail no Free), "signingOrder"?: number }
  201 → { "id": "sgn_…", "status": "PENDING", "cpfMasked": "***.456.789-**" }
- POST https://api.assinapi.com.br/v1/envelopes/{envelopeId}/send
  body: { "notify"?: boolean }   (notify=false → você envia o link por outro canal)
  200 → { "status": "SENT", "signingLinks": [{ "signerId", "name", "signingUrl", "expiresAt" }] }
  Os signingLinks só aparecem NESTA resposta: salve-os se precisar.
- GET https://api.assinapi.com.br/v1/envelopes/{envelopeId}
  200 → { "status": "DRAFT"|"SENT"|"IN_PROGRESS"|"COMPLETED"|"DECLINED"|"EXPIRED"|"CANCELLED", "signers": [{ "status" }] }
- GET https://api.assinapi.com.br/v1/envelopes/{envelopeId}/download   (somente após COMPLETED)
  200 → arquivo PDF assinado, com relatório de assinaturas e auditoria (IP, localização, dispositivo, trilha). ?type=certificate → certificado de evidências.
  Baixe e armazene no seu storage.

Atalho recomendado (uma chamada só): POST https://api.assinapi.com.br/v1/signature-requests
  body: { "title": string, "document": { "filename": string, "contentBase64": string }, "externalId"?: string, "signers": [{ "name", "email", "cpf", "birthDate"?, "fields"?: [{ "anchor": "{{assinatura_cliente}}" } | { "page": "last", "x": 60, "y": 80 }], "requireDocumentPhoto"?: boolean, "requireSelfie"?: boolean }], "redirectUrl"?: string, "reminderIntervalDays"?: number }
  201 → { "id": "sigreq_…", "envelopeId": "env_…", "status": "SENT", "signingUrl": string, "signers": [{ "id", "signingUrl" }] }  — salve o id (sigreq_…).
  GET https://api.assinapi.com.br/v1/signature-requests/{id} → { "status", "completed": boolean, "nextStep": string, "downloads": { "signedPdfUrl" } | null }
  GET https://api.assinapi.com.br/v1/signature-requests/{id}/download → PDF assinado. POST …/{id}/remind (lembrete) e POST …/{id}/cancel.

## Idempotência e retries
- Envie `Idempotency-Key: <uuid>` em todo POST. Em retry (timeout, 429, 5xx) reenvie com a MESMA chave.
- Em 429 respeite o header Retry-After e use backoff exponencial.

## Tratamento de erros
Erros têm o formato { "error": { "code", "message", "hint", "docs", "requestId" } }. Trate pelo `code` (estável), exiba `message` ao usuário quando fizer sentido e registre o `requestId` para suporte.
Códigos comuns: VALIDATION_FAILED, CPF_REQUIRED, INVALID_CPF, ENVELOPE_INCOMPLETE, INVALID_STATE_TRANSITION, INVALID_API_KEY, RATE_LIMITED.

## Webhook
- Registre o endpoint: POST https://api.assinapi.com.br/v1/webhooks { "url": "https://…/api/webhooks/assinapi", "events": ["envelope.completed", "signer.signed"] } → guarde o "secret" retornado (exibido uma vez).
- Headers recebidos: X-Signature (v1=<hex>), X-Event-Id, X-Timestamp.
- Valide: HMAC_SHA256(ASSINAPI_WEBHOOK_SECRET, `${X-Event-Id}.${X-Timestamp}.${corpo_bruto}`) === X-Signature (comparação em tempo constante).
- Rejeite X-Timestamp com mais de 5 minutos; deduplique por X-Event-Id; responda 2xx rápido.
- Todo evento: data.envelope = { "id" (env_…), "signatureRequestId" (sigreq_…), "status", "externalId" }.
- Ao receber envelope.completed (o PDF já está pronto), marque o registro como FINALIZADO e baixe o PDF em GET /signature-requests/{signatureRequestId}/download.
- signer.signed = assinatura parcial; signer.declined / envelope.expired / envelope.cancelled = encerrado sem assinatura; signer.identity_submitted = fotos aguardando aprovação.

## Entregáveis
- `assinapi.service` com funções tipadas para cada operação.
- Persistir o id (sigreq_…) e o envelopeId junto ao registro do seu sistema, e enviar o seu id em externalId.
- Variáveis de ambiente documentadas (.env.example sem valores reais).
- Testes da camada de serviço com a API mockada.

Edge Function que recebe o webhook

typescript
// supabase/functions/assinapi-webhook/index.ts
Deno.serve(async (req) => {
  const raw = await req.text();
  const secret = Deno.env.get('ASSINAPI_WEBHOOK_SECRET')!;
  const eventId = req.headers.get('x-event-id') ?? '';
  const ts = Number(req.headers.get('x-timestamp'));
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return new Response('stale', { status: 400 });
  const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
  const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${eventId}.${ts}.${raw}`));
  const expected = 'v1=' + [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');
  if (expected !== req.headers.get('x-signature')) return new Response('invalid signature', { status: 401 });
  const event = JSON.parse(raw);
  // ex.: atualizar tabela contracts onde assinapi_envelope_id = event.data.envelope.id
  return new Response('ok');
});

Exemplos apontam para o Sandbox (https://api.assinapi.com.br/v1). Em Produção, troque apenas a chave (ask_live_…); o endereço é o mesmo.

Perguntas frequentes

Perguntas técnicas.

Por que não chamar a AssinAPI direto do React?

Porque a secret key ficaria exposta no navegador. Qualquer pessoa poderia enviar documentos em seu nome. A Edge Function é o backend do app Lovable.

A Edge Function do webhook precisa de JWT?

Não. Configure verify_jwt = false: a autenticidade do webhook é garantida pela assinatura HMAC (X-Signature) com o seu ASSINAPI_WEBHOOK_SECRET.

Como o signatário se autentica? Dá para usar WhatsApp?

WhatsApp está nos planos pagos. Simples: por padrão só o link por e-mail (EMAIL_LINK). Avançada (CPF e data de nascimento conferidos): nos planos pagos, link por e-mail + código pelo WhatsApp (WHATSAPP_OTP) ou só WhatsApp (WHATSAPP_LINK); no Free, link e código por e-mail (EMAIL_OTP), e pedir WhatsApp devolve 402 WHATSAPP_REQUIRES_PAID_PLAN. Envie o celular em phone (+5511999999999) e, se quiser, defina o padrão no projeto.

Consigo testar sem custo?

Sim. O Sandbox é gratuito: chaves ask_test_…, código de confirmação fixo 123456 e CPFs de teste. Documentos de Sandbox não têm validade e não consomem a franquia do plano.

Primeira assinatura no Sandbox em menos de 10 minutos.

Sem cartão de crédito. Sandbox gratuito; você só paga por documentos enviados em Produção.