AssinAPI

Integrando com Next.js

Use Route Handlers ou Server Actions (server-side). Deploy na Vercel com a chave em Environment Variables.

Secret key

Vercel → Settings → Environment Variables → ASSINAPI_SECRET_KEY (sem NEXT_PUBLIC_).

Arquitetura

Client Component → Route Handler / Server Action → AssinAPI. Webhook → app/api/webhooks/assinapi/route.ts.

Prompt pronto

markdown
Implemente a integração com a AssinAPI neste projeto. Analise a stack existente e siga as convenções do código.

## 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://sandbox.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://sandbox.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://sandbox.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://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}/signers
  body: { "name": string, "email": string, "cpf": string, "phone"?: string, "authenticationMethod"?: "EMAIL_OTP"|"SMS_OTP", "signingOrder"?: number }
  201 → { "id": "sgn_…", "status": "PENDING", "cpfMasked": "***.456.789-**" }
- POST https://sandbox.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://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}
  200 → { "status": "DRAFT"|"SENT"|"IN_PROGRESS"|"COMPLETED"|"DECLINED"|"EXPIRED"|"CANCELLED", "signers": [{ "status" }] }
- GET https://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}/evidence   (somente após COMPLETED)
  200 → { "downloads": { "documents": [{ "final": url, "original": url }], "certificate": url, "manifest": url }, "verifyUrl": string }
  As URLs expiram em minutos: baixe e armazene no seu storage, não salve a URL.

Atalho (uma chamada só): POST https://sandbox.api.assinapi.com.br/v1/signature-requests
  body: { "title": string, "document": { "filename": string, "contentBase64": string }, "signer": { "name", "email", "cpf" }, "authentication": "EMAIL_OTP" }
  201 → { "id": "env_…", "status": "SENT", "signingUrl": string, "signers": [...] }

## 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://sandbox.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.
- Payload envelope.completed: { "id", "type", "data": { "envelope": { "id", "status", "externalId" }, "evidence": { "manifestHash", "verifyUrl" }, "documents": [...] } }
- Ao receber envelope.completed, atualize o status no banco e (se aplicável) baixe o PDF final via GET /envelopes/{id}/evidence.

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

Criar envelope

  • Cria um envelope (rascunho).
  • Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/create-envelope/route.ts
export async function POST(request: Request) {
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify({
      "title": "Contrato de prestação de serviços",
      "expirationDays": 7
    }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return Response.json(data, { status: res.status });
}

Enviar PDF

  • Anexa o PDF ao envelope. O SHA-256 é calculado pela AssinAPI.
  • Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/upload-pdf/route.ts
export async function POST(request: Request) {
  const form = new FormData();
  form.append('file', await req.blob(), 'contrato.pdf');
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/documents', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}` },
    body: form,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return Response.json(data, { status: res.status });
}

Adicionar signatário

  • Adiciona um signatário (CPF obrigatório na assinatura avançada).
  • Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/add-signer/route.ts
export async function POST(request: Request) {
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/signers', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify({
      "name": "João da Silva",
      "email": "joao@email.com",
      "cpf": "529.982.247-25",
      "authenticationMethod": "EMAIL_OTP"
    }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return Response.json(data, { status: res.status });
}

Enviar para assinatura

  • Envia para assinatura. A resposta traz os signingLinks.
  • Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/send/route.ts
export async function POST(request: Request) {
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/send', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify({
      "notify": true
    }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return Response.json(data, { status: res.status });
}

Consultar status

  • Consulta status do envelope e dos signatários.
  • Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/status/route.ts
export async function POST(request: Request) {
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID', {
    method: 'GET',
    headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}` },
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return Response.json(data, { status: res.status });
}

Baixar documento assinado

  • Retorna URLs temporárias do PDF assinado, certificado e manifesto.
  • Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/download/route.ts
export async function POST(request: Request) {
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/evidence', {
    method: 'GET',
    headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}` },
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return Response.json(data, { status: res.status });
}

Receber webhooks

  • Valide SEMPRE a assinatura HMAC (X-Signature) usando o corpo bruto.
  • Rejeite eventos com X-Timestamp com mais de 5 minutos e deduplique por X-Event-Id.
  • Responda 2xx rapidamente; processe de forma assíncrona. Falhas são reenviadas com backoff exponencial.
typescript
// 1) Registrar endpoint
/*
curl -X POST "https://sandbox.api.assinapi.com.br/v1/webhooks" \
  -H "Authorization: Bearer $ASSINAPI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://seu-sistema.com/api/webhooks/assinapi", "events": ["envelope.completed", "signer.signed"] }'
# Guarde o 4 (whsec_…) retornado em ASSINAPI_WEBHOOK_SECRET
*/

// 2) Validar cada requisição recebida
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyAssinapiWebhook(rawBody: string, headers: Headers, secret = process.env.ASSINAPI_WEBHOOK_SECRET!) {
  const signature = headers.get('x-signature') ?? '';
  const eventId = headers.get('x-event-id') ?? '';
  const timestamp = Number(headers.get('x-timestamp'));
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = 'v1=' + createHmac('sha256', secret).update(`${eventId}.${timestamp}.${rawBody}`).digest('hex');
  return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}