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
O usuário clica em “Enviar para assinatura” no app (React).
- 2
O app chama supabase.functions.invoke("assinapi-send").
- 3
A Edge Function lê ASSINAPI_SECRET_KEY e chama POST /v1/signature-requests.
- 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
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
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
// 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.